[Makefile] 不完全指导手册
make
是最常用的也是最经典的 build 工具,然而我却现在才开始用这个家伙。简单地说,只要自行规范好 build 顺序,只需要一句简单的 make
就可以解决所有问题。正如其历史所言,一开始 make
常用于构建C项目,但实际上只需要提供好编译命令,任何项目都可以用 make
构建生成。
比如这里有一个简单的Windows环境下生成文档的例子
TEX = texify
MAIN = assignment
TEXFLAGS = --pdf --engine=xetex --tex-option=-synctex=1
target: clean tex open
tex: $(MAIN).tex
$(TEX) $(TEXFLAGS) $<
open: $(MAIN).pdf
cmd /c start $(MAIN).pdf
clean:
del -f *.aux *.bbl *.blg *.log *.out *.gz *.toc *.pdf
.PHONY: clean
每次写好最终的 assignment.tex 文档之后,只需要执行一个 make
就可以编译并打开生成的文档了。
上手
当然上面的那个例子有点过分了,实际上有更简单的一个例子
main: main.cpp
g++ -o main main.cpp
当我们写好自己的 main.cpp 文件之后,可以直接用 make main
命令制作出可执行文件 main
,这里调用的命令便是 Makefile 文件的第二行 g++ -o main main.cpp
。简而言之,所有的 Makefile 文件可以看做一个“制作名单”,每一个“作品”由三部分构成:名字,依赖项,制作方法。如下所示,其中 ()*
表示可以有0个或多个。
<makeitem> := {
<target>: (<dependency>)*
(<TAB><Command>)*
}
自然,如果有多个依赖项,make
会先自行检查本地目录有没有,随后检查自己是否可以直接构建,如果都没有就会直接报错结束这次构建任务。如果成功构建(或寻得)了这次构建任务的依赖项,便会开始这次构建任务,即依次执行具体的编译指令。之所以叫具体的编译指令而不是命令,是因为 make
并不能完全接受大部分的命令,比如 Windows 下的 start 。由于 start 是一个命令而非文件,所以 make 会返回错误,这里应该使用 cmd /c start
来代替直接使用 start
。而 Linux 下,大部分所用的命令其本质为可执行文件,所以一般情况下不必担心这个问题。
>make start
start assignment.pdf
process_begin: CreateProcess(NULL, start assignment.pdf, ...) failed.
make (e=2): 系统找不到指定的文件。
makefile:14: recipe for target 'start' failed
make: *** [start] Error 2
正是由于依赖项的存在,使得 make
可以作为小型项目的构建工具,使得其有顺序的“制作”所需项目。比如这样一个简单的例子,我们手上有三个文件 a.h, a.cpp, main.cpp,其中 a.h a.cpp 是类 one_class 的定义与具体实现,main.cpp使用了这个类。我们的 Makefile 可以这样写
main: main.o a.o
g++ -o main main.o a.o
main.o: main.cpp
g++ -c main.cpp
a.o: a.cpp a.h
g++ -c a.cpp a.h
如此一来便可以通过 make main
直接构建出最后的可执行文件 main 。同样也可以只写 make
,因为 make
会默认构建 Makefile 文件中的第一个目标。
添加变量
有的时候你会发现你的代码用到了 C++11,所以你不得不在编译命令里加上一句
main: main.cpp
g++ -o main main.cpp -std=c++11
然后你发现你忘记了正确链接数学基本库
main: main.cpp
g++ -o main main.cpp -std=c++11 -lm
然后你发现所有的命令都要加上这两条
main: main.o a.o
g++ -o main main.o a.o -std=c++11 -lm
main.o: main.cpp
g++ -c main.cpp -std=c++11 -lm
a.o: a.cpp a.h
g++ -c a.cpp a.h -std=c++11 -lm
然后你发现你违反了DRY(Don’t Repeat Yourself)原则,所以你设置了一个变量叫做 CXXFLAGS 用来记录所有的编译选项,
CXXFLAGS = -std=c++11 -lm
main: main.o a.o
g++ -o main main.o a.o $(CXXFLAGS)
main.o: main.cpp
g++ -c main.cpp $(CXXFLAGS)
a.o: a.cpp a.h
g++ -c a.cpp a.h $(CXXFLAGS)
紧接着你发现部署环境里用的是 clang,开发环境用的是 g++
CC = g++ # clang
CXXFLAGS = -std=c++11 -lm
main: main.o a.o
CC -o main main.o a.o $(CXXFLAGS)
main.o: main.cpp
CC -c main.cpp $(CXXFLAGS)
a.o: a.cpp a.h
CC -c a.cpp a.h $(CXXFLAGS)
这样只需要在不同的地方修改一处注释就行了。
除了这些基本用法意外,make还可以直接调用shell的变量
test:
echo $$JAVA_HOME
@echo $$JAVA_HOME
在这里,两个 $$
表示正常环境下的一个 $
,也可以理解为,$
有转义字符的意思。而第三行的 @...
表示关闭 echo,即没有命令回显。
另外针对变量 makefile 有一些运算符[ref 1]
VARIABLE = value # lazy
VARIABLE := value # immediate
VARIABLE ?= value # if absent
VARIABLE += value # append
除此之外,make
还提供了一些简单的内置变量,诸如 $(CC)
表示默认的C编译器(cc),$(CXX)
表示默认的 C++ 编译器。具体的可以查看链接[ref 2]浏览。
自动变量
真正有意义的我觉得还是这个自动变量,因为你不可能为每一个目标单独写构建命令。常见的自动变量有这几个:
$@
,$<
,$^
,$*
,分别表示,构建目标,第一个前置条件,所有前置条件,和匹配符%匹配的部分。
a.txt: b.txt c.txt
echo $@ # => a.txt
echo $< # => b.txt
echo $^ # => b.txt c.txt
%.o: %.cpp %.h
g++ -o $* %^ -std=c++11
这样相当于可以轻松地解决大量的重复工作。比如之前的C++项目就可以直接写成这种形式:
CC = g++ # clang
EXEC = main
OBJ = a.o b.o main.o
CXXFLAGS = -std=c++11 -lm
$(EXEC):$(OBJ)
$(CC) -o $(EXEC) $(OBJ) $(CXXFLAGS)
main.o: main.cpp
$(CC) -c main.cpp $(CXXFLAGS)
%.o: %.cpp %.h
$(CC) -c $^ $(CXXFLAGS)
clean:
rm -f *.o *.exe
.PHONY: clean
更多的自动变量可以查看链接[ref 3]浏览。
Reference
[1]: Makefile variable assignment - stackoverflow
[2]: Implicit-Variables
[3]: Automatic-Variables
[4]: GNU make