如何写 makefile
对于源文件较多的 c/c++项目,直接在 shell 中使用 gcc/clang 进行编译会十分麻烦,makefile 可以解决这一问题。Makefile 记录了项目的编译规则,当使用 make 命令进行项目的编译时,make 命令会使用 makefile 中记录的规则,一步步地编译、链接,生成目标程序;同时已经 make 过的项目再次 make 时,只会重新编译发生修改过的源文件。这些特性都能够节省编译时间。
makefile 文件包含五种成分:显式规则、隐晦规则、变量定义、文件指示、注释。其中隐晦规则指 make 命令能够自动推导的命令,可以省略规则中的某些元素;文件指示指 makefile 引用其它 makefile,或者指定使用 makefile 哪一部分的规则。一个 makefile 的基本成分如下所示:
target : prerequisites command # target 为该步骤的目标,可以是目标文件、执行文件、标签 # prerequisites 为目标所依赖的成分,可以是源文件、目标文件、标签 # command 为生成上述目标所进行的shell命令 # 目标 : 依赖 # shell命令
注释
makefile 中使用 #
来标记注释。
显示规则
下面是一个 makefile 文件的例子。
# 使用 main.cpp, a.cpp, b.cpp, c.cpp 生成目标程序 main # --------------------- main : main.o a.o b.o c.o c++ -o main main.o a.o b.o c.o main.o : main.cpp c++ -c main.cpp a.o : a.cpp c++ -c a.cpp b.o : b.cpp c++ -c b.cpp c.o : c.cpp c++ -c c.cpp .PHOMY : clean # 声明 clean 为一个伪目标 clean : # 没有依赖的规则,make命令不会自动执行,需要使用形如 make clean 这样的形式才能执行 rm main *.o # --------------------
下面是 makefile 中生成规则的一些细节:
- 通过比较目标与它的依赖间修改时间的差异,只要依赖中有比目标新的文件,就重新生成目标。
- 在一条规则中,可以有多个目标,使用空格分割;可以有多条 shell 命令。
- 多条 shell 命令可以放入多行,此时 shell 命令会放入不同的进程执行;如果后面 shell 命令依赖前面 shell 命令的效果,则两条 shell 命令使用
;
分割。\
可以折行。 - make 命令默认将每一条 shell 命令打印出来,如果不想打印命令,在 shell 命令前加
@
。
如果不要求 make 检查 shell 命令的执行是否正确,在其前加-
。 - makefile 中可以使用的通配符与 shell 中相同,
*
?
;也使用~
指代用户文件夹。
上述例子为生成执行程序的规则,下面介绍生产静态库(Archive File)的规则:
name_of_lib(a.o) a.o ar rc name_of_lib a.o
使用变量
makefile 文件中的变量,类似于 c 文件中的宏定义,起替换文本的作用。下面是一个使用变量的例子。
objects = main.o a.o b.o c.o main : $(object) c++ -o main $(object)
使用变量时的四种赋值运算符:
x = $(y)
懒求值,在运行时求值;x := $(y)
立即求值,在定义时求值;x ?= $(y)
只有当 x 不存在时才定义 x;x += $(y)
追加。
上述四种运算符中的前两种比较容易让人产生迷惑,下面以一个例子说明其差别:
# 使用 = x = hello y = $(x) world echo $(y) # -> hello world x = hi echo $(y) # -> hi world # 使用 := x = hello y := $(x) world # 立即求值,在这里已经将 y 中的 $(x) 替换了,后面 x 的变化不会影响 y echo $(y) # -> hello world x = hi echo $(y) # -> hello world
赋值运算符另一个需要注意的地方是:不要在赋值语句的后面添加注释,因为注释前的空格会添加到变量里面,产生不可预期的结果。
变量的高级用法——变量值替换
sources = a.cpp b.cpp c.cpp objects = $(sources:%.cpp=%.o)
内置变量
$(AR) # 生产 archive 文件的默认程序 ar $(CC) # 编译 C 代码的默认编译器 cc $(CXX) # 编译 C++ 代码的默认编译器 g++ $(ARFLAGS) # ar 的参数 ‘rv‘ $(CFLAGS) # 编译 C 代码的参数 $(CXXFLAGS) # 编译 C++ 代码的参数 $(CPPFLAGS) # C 代码预编译的参数
条件
objects = main.o a.o b.o c.o main : objects ifeq ($(CXX), g++) # 这里的 $(CXX) 是内置变量 $(CXX) -o main $(objects) $(libs_for_g++) else $(CXX) -o main $(objects) $(normal_libs) endif # 其他的 ifneq, ifdef var_name, ifndef var_name
函数
# (function arguments) $(wildcard src/*.cpp) # 通配符匹配的部分 $(subst .cpp,.o,$(sources)) # 字符串替换 $(patsubst %.cpp,%.o,$(sources)) # 模式字符串替换 $(strip $(string)) # 去掉字符串开头和结尾的空格 $(dir src/foo.c) # 取文件目录,得到 src/ $(notdir src/foo.c) # 取文件名,得到 foo.c $(suffix src/foo.c) # 取尾缀,得到 .c $(basename src/foo.c) # 取前缀,得到 src/foo $(addsuffix .c,src/foo) # 加尾缀,得到 src/foo.c $(addprefix src/,foo.c) # 加前缀,得到 src/foo.c $(foreach n,$(names),$(n).o) # 循环,将 names 中的每一个都添加 .o $(shell cat foo) # 执行 shell 命令 $(warning "test") $(error "test")
隐晦规则
一些在 makefile 经常用到的规则,make 会为我们自动生成。下面是一些常用的隐含规则:
编译 C 代码:自动将 name.c 生成 name.o,生成命令为
$(CC) -c $(CPPFLLAGS) $(CFLAGS)
。编译 C++ 代码:自动将 name.cc 生产 name.o,生成命令为
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)
。链接 Object 文件:自动将一个或多个 Object 文件链接成执行文件。如
main : main.o a.o
,将会执行下面的命令:cc -c main.c -o main.o cc -c a.c -o a.o cc main.o a.o -o main rm -f main.o rm -f a.o
生产静态库的规则也可以自动推导,如
name_of_lib(a.o b.o)
将会执行下面的命令:cc -c a.c -o a.o cc -c b.c -o b.o ar r name_of_lib.a a.o b.o rm -f a.o rm -f b.o
使用模式规则
%
在 makefile 中表示任意长度的非空字符串,模式规则指使用 %
定义的规则,可以达到一条规则处理多个目标的目的。下面以例子说明模式规则以及自动变量的用法:
# 将所有的 .cpp 文件编译成对应的 .o 文件 %.o : %.cpp $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< -o # 为当前目标,$< 为当前第1个依赖,这两个为自动变量。 # 类似于迭代器,每次会取一个目标和依赖执行命令,将所有符合模式的文件全部迭代。
总结一下常用的自动变量:
# 目标 $< # 第1个依赖 $? # 比目标新的依赖 $^ # 所有依赖,会去除重复的 $+ # 所以依赖,不会去除重复的 $* # 模式匹配到的部分,即 % 代表的部分 $(@D) $(@F) # 的目录名和文件名 # 其他的自动变量也可以构造类似的,获得目录名和文件名
文件指示
文件指示指在 makefile 中引用其他的 makefile 文件。
include foo.make *.make # 引用其他的 makefile
make 命令
在 make 命令使用中经常看到的 all、clean、install、test 等,都是伪目标,需要在 makefile 中制定这些伪目标所要求的行为。
make 命令的常用参数:
-f filename # 指定makefile文件 -I include_dir # 在该目录下搜索 makefile -j jobnum # 指定 job 数量,默认 make会运行尽量多的 job