说明
是选择cmake还是make,这个问题不用纠结。make必须会,自己平时写小程序,一定要会make。而cmake有时候组织一些大工程会很有用,这里不是说make不能做大工程。
make涉及到源文件到目标文件,目标文件到可执行文件,如果这些概念不清楚,先将这些概念性东西理解清楚,更加方便下面的内容。
编写测试文件时小技巧:
如果在vim中编辑Makefile文件,可以在命令行模式下输入 ‘:! make target’ 快速测试,不需要再退出Makefile在shell终端执行make target。
本文档很多东西没有涵盖,也没有做过多的讲解,详细请参考官方make文档。如果已经对Makefile有一点基础,建议直接看实战项目。
Makefile语法规则
target ... : prerequisites ...
receipe
...
...
target是可执行文件或目标文件的名字。(还有一种情况就是伪目标,后面讲)
prerequisites是一个文件,使用这些文件创建最后的目标。
recipe是一个make执行的命令。
注意:每一行的recipe之前一定要有一个tab字符。
说明:Makefile文件名不能更改,除非你指定make读取指定的文件(不建议这种做法)。
简单例子
main : main.o hello.o foo.o
g++ -o main main.o hello.o foo.o
main.o : main.cpp hello.h foo.h
g++ -c main.cpp
hello.o : hello.cpp hello.h
g++ -c hello.cpp
foo.o : foo.cpp foo.h
g++ -c foo.cpp
.PHONY : clean
clean :
rm main main.o hello.o foo.o
上面逻辑很清楚,用源文件创建出目标文件,然后将所有的目标文件创建出最后的可执行文件。clean是一个伪目标,执行make clean将清除可执行文件main,main.o,hello.h和foo.h。
简单例子升级(使用变量)
使用变量来缩减代码量。
objects = main.o hello.o foo.o
main : $(objects)
g++ -o main $(objects)
main.o : main.cpp hello.h foo.h
g++ -c main.cpp
hello.o : hello.cpp hello.h
g++ -c hello.cpp
foo.o : foo.cpp foo.h
g++ -c foo.cpp
.PHONY : clean
clean :
rm main $(objects)
说明:变量实际上就是进行了内容的替换。
变量不用声明,可以直接使用,使用$(variable)获取变量的值。
简单例子升级(make自动推断)
实际上make可以自动推断recipes,如果你的target是目标文件,prerequisite是源文件,那么可以省去g++ -o xxx.cpp。看下面代码:
objects = main.o hello.o foo.o
main : $(objects)
g++ -o main $(objects)
main.o : main.cpp hello.h foo.h
hello.o : hello.cpp hello.h
foo.o : foo.cpp foo.h
.PHONY : clean
clean :
rm main $(objects)
以上代码省略了recipes,让make自动推断,编译出目标文件。
实际上,上面代码还可以省:
objects = main.o hello.o foo.o
main : $(objects)
g++ -o main $(objects)
$(objects) : hello.h foo.h
.PHONY : clean
clean :
rm main $(objects)
上面将上面三条进行合并,这些目标文件并不需要按照顺序,make会根据源代码自动推断,生成目标文件。
Makefile组成
Makefile可以说非常像shell脚本,它里面有显式规则,隐式规则,变量,指令和注释。
下面将粗略讲讲各个组成部分
显式规则:列出所有的prerequisite和recipes,参考:简单例子。
隐式规则:用make自动推断,参考:简单例子升级(make自动推断)。
变量:包括自定义变量和内置变量。
指令:include,define等指令。include指令可以包含其它Makefile,define指定可以定义变量。
注释:以 ‘#’ 开头的一行。
提示: 当一行内容特别长,可以使用反斜杠 ‘\’ 来分割一行为两行。
读取其他Makefile可以使用include指令。
recipes中可以使用shell变量,但是有时候并不是直接使用指令。例如创建一个子系统时,通常会使用如下:
substem:
cd subdir && $(MAKE)
这里不能显式使用make,而要使用$(MAKE)。
变量
变量分为自定义变量和内置变量。
自定义变量不需要声明,可以直接使用,使用$(variable)
表示variable中的值,内置变量就是make中的一些预留变量,代表一些固定的值,例如$(MAKE)。
可以使用define指令定义一个变量,变量名中不能包含 ‘:’, ‘#’, ‘=’和空格等符号。
变量名大小写敏感,foo和FOO表示不同的变量,可以使用$(foo)
或${foo}
取出变量foo中值。变量能够在target,prerequisite和recipe等地方使用。
记住,变量遵循严格的文本替换。
foo = c
prog.o : prog.$(foo)
g$(foo)$(foo) -$(foo) prog.$(foo)
上面代码可以转换为一下内容。
prog.o : prog.c
gcc -c prog.c
变量的赋值
变量名的赋值除了使用 = 外,还可以使用 :=, ::=, ?= 和 +=。
:= 符号用于覆盖变量值并写入新值。
whoami := $(shell whoami)
whoami := byy
show:
@echo $(whoami)
使用make show,会输出byy。
说明:可以使用$(shell command)来执行shell命令。
那么问题来了,为什么会同时存在 = 和 := 两个符号?每次赋值不是都可以更新值吗,看看下面例子:
x = foo
y = $(x) bar
x = abc
show:
@echo $(y)
执行make show显示结果: abc bar。
x := foo
Y := $(x) bar
x := abc
show:
@echo $(y)
执行make show显示结果:foo bar。
总结::= 符号取决于在makefile中的位置,变量会最后在Makefile中展开,而 “=” 会用变量最后展开的值。
?= 表示条件变量赋值操作符。
如果左边的变量已经定义,那么就将右边的值赋给变量,否则,不会进行赋值操作。
+= 表示在变量原有的情况下,增加一个新的内容,在写Makefile会非常有用。
ojects += another.o
变量值中的替换操作
foo := a.o b.o c.o
bar := $(foo:.o=.c)
show:
@echo $(bar)
将.o全部替换为.c,显示结果为: a.c b.c c.c
多行变量
define two-lines =
echo foo
echo $(bar)
endef
如果变量右边的内容太多,可以使用多行变量。上面的代码等同于以下代码:
two-lines = echo foo; echo $(bar)
取消变量定义
使用 undefine 变量名 可以将一个变量变为undefined。
特殊变量
$< 表示第一个prerequisite
$^ 表示所有的prerequisite
$@ 表示目标
VPATH指定prerequisite的搜索目录
VPATH = src:../headers
...
foo.o : foo.c
如果当前目录没有foo.c,那么会到src目录下去搜foo.c文件是否存在,如果还是没有找到那么会到../headers。
vpath指定搜索路径下的指定类型的文件名
以下是vpath的用法:
vpath pattern directories
指定搜索目录下匹配的文件名
vpath pattern
清除与pattern相关的搜索路径
vpath
清除前面指定的所有查找路径
示例:vpath %.h ../headers
结果:如果当前目录下没有找到.h类型文件,那么会到../headers目录下进行查找。
说明: vpath是指令,在Makefile中,大写的一般是变量,小写一般是指令。
条件判断
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
条件判断的种类:ifeq, ifneq, ifdef, ifndef。
内置函数
字符串操作
$(subst from, to, text)
作用:字符串中子字符串替换,返回替换之后的结果。
例子:$(subst ee, EE, feet on the street)
结果: ‘fEEt on the strEEt’
$(patsubst pattern, replacement, text)
作用:字符串模式匹配,进行替换,并返回替换之后的结果。
例子:$(patsubst %.c, %.o, x.c.c bar.c)
结果:‘x.c.o bar.o’
上面可以简写: $(objects:.c=.o)
$(strip string)
作用:去掉字符串开头和结尾的空格。
去掉字符串开头和结尾的空格可以让代码更加具有健壮性。
$(findstring find, in)
作用:在字符串中查找字符串find,如果找到,则返回find,否则返回‘’
例子:$(findstring a,a b c)
结果:返回 ‘a’
还有很多关于文本替换的函数,这里不做过多的介绍。
foreach函数
$(foreach var,list,text)
作用: 遍历list,执行text。
wildcard函数
通配符函数,可以出现在Makefile的任意位置。
小技巧,看下面例子:
$(wildcard *.c)
上面语句可以获取当前目录下所有的.c文件名。
下面语句可以将所有的.c文件变为.o文件。
$(wildcard %.c,%.o,$(wildcard *.c))
$(wildcard pattern…)
wildcard可以在任何地方使用。
文件名函数
$(basename names...)
作用:去掉文件名后缀。
例子:$(basename src/foo.c src-1.0/bar hacks)
结果:src/foo src-1.0/bar hacks
$(addprefix prefix,names...)
作用:增加前缀。
例子:$(addprefix src/,foo bar)
结果:src/foo src/bar
$(addsuffix suffix,names...)
作用:增加后缀。
例子:$(addsuffix .c,foo bar)
结果:foo.c bar.c
项目实战
看文件目录结构
├── bin
├── Makefile
├── obj
└── src
├── foo
│ ├── foo.cpp
│ └── foo.h
├── hello
│ ├── hello.cpp
│ └── hello.h
└── main.cpp
实际项目中,需要分模块将hello和foo当做两个模块。现在如何编写Makefile?
CXXFLAGS = -Wall
LINK := $(CPP)
vpath %.cpp src
vpath %.cpp src/hello
vpath %.cpp src/foo
OBJ_PATH := obj
BIN_PATH := bin
SRC_PATH := src/hello src/foo src
INCS = -I src/hello -I src/foo
SRC = $(foreach x,$(SRC_PATH),$(wildcard $(addprefix $(x)/*,.c .cpp)))
OBJ = $(addprefix $(OBJ_PATH)/, $(addsuffix .o,$(notdir $(basename $(SRC)))))
TARGET = main
$(BIN_PATH)/$(TARGET): $(OBJ)
$(CXX) $(CXXFLAGS) -o $@ $^
$(OBJ_PATH)/%.o: %.cpp
$(CXX) -c $(CXXFLAGS) $(INCS) -o $@ $<
.PHONY: clean
clean:
rm -f $(OBJ_PATH)/* $(BIN_PATH)/*
总结
Makefile中尽量显示出模块化的思想,尽量使用变量带代替重复的内容,不会导致改变一处而全局改变。Makefile的编写中,最重要是当做一门语言来学,学会看错误提示,不断调试。
FAQ
Q: 如何指定目录放指定文件?
A: 在编写Makefile时指定前缀。
$(OBJ_PATH)/$(TARGET):$(OBJ)
$(CPP) -o $@ @^
参考
[1] gnu make官网:http://www.gnu.org/software/make/manual/make.pdf
[2] Makefile模板 :http://www.oschina.net/code/snippet_113073_38647