通常一个大型程序由多个模块文件构成的,按照其功能划分,模块文件会分布在不同的目录中,模块文件之间有包含有头文件,调用函数的情况,他们之间存在依赖关系。
大多数情况下,我们只是修改了其中某些文件,而不是所有文件,按理说,我们只需要把那些修改过的文件,并且依赖于这些文件的相关文件重新编译即可,用不着重新编译全部文件。
所以问题来了,有没有办法自动对那些有改过的文件重新编译呢?这实际上要分为 2 个问题:
- 目标文件依赖哪些文件?
- 依赖的文件是否更新?
为了解决这2个问题,业界就有了 make 和 makefile。总的来说,它俩并不是用来编译程序,它们只负责找出哪些文件变化,并且根据依赖关系找出受影响的文件,然后执行事先在 makefile 中定义好的命令规则。只是该规则大多数情况是调用 gcc 或 nasm 进行编译。因此这也就完成了我们上面提到的 2 个问题。
makefile 基本语法
目标文件:依赖文件
[tab] 命令
eg:
a.o: a.c
gcc -c a.c -o a.o
a.out: a.o
gcc a.o -o a.out
- 目标文件是指此规则中想要生成的文件,可以是 .o 结尾的目标文件,也可以是可执行文件,也可以是伪目标。
- 依赖文件是指要生成此规则中的目标文件。通常不止一个,是一个依赖文件的列表。
- 命令是指此规则中要执行的动作,各种 shell命令,一个命令单独占用一行,行首必须以 Tab 开头。
根据以上规则,我们已经知道了目标文件依赖哪些文件,但还需要解决第2个问题。怎么判断依赖的文件是否更新?
在 Linux 中,每个文件有3种时间:
- atime
access time 访问文件数据部分时间 - ctime
change time 文件属性部分或数据部分的改变时间 - mtime
modify time 文件数据部分的修改时间
1:2
@echo "makefile test ok"
这个makefile文件的意义就是 " 如果文件2的 mtime 比文件1的 mtime 要新,则打印 "makefile test ok" "。
综上,依赖关系定义在文件 makefile 中,make 程序通过解析 makefile 文件,根据mtime标签自动找出 变更的文件 以及 依赖此变更文件的目标文件,然后对所有受影响的相关文件执行事先定义好的命令规则。
makefile 的文件名并不固定,可以用 -f 参数指定。默认是先寻找名为 GNUmakefile 的文件,若该文件不存在再去找名为 makefile 的文件,若 makefile 也不存在,最后去找名为 Makefile 的文件。
t1:1
@echo "target1"
t2:1
@echo "target2"
若采用make 目标名称
,比如make t2
,则会单独执行目标名称处的规则。
若采用make
,则会单独执行在 makefile 中第一个出现的目标。
变量
= := ?= 区别
目标变量
伪目标
有时我们有"并不关心是否产生真实的目标文件,我们只希望通过 make 不要考虑 mtime,而是总能去执行一些命令",比如
clean:
rm *.o # 清理文件
这时,make规定,当规则中不存在依赖文件时,这个目标文件名就称为 -- 伪目标。
.PHONY
约定俗成的伪目标名称
伪目标名称 | 功能描述 |
---|---|
all | 通常用于完成所有模块的编译工作 |
clean | 通常用于清空编译完成的所有目标文件 |
dist | 将打包文件再压缩成gz 文件 |
Install | 通常将编译好的程序复制到安装目录下 |
printf | 通常用于打印已经发生改变的文件 |
tar | 通常用于将文件打包成tar文件 |
test | 测试makefile 流程 |
make: 递归式推导目标
test1.o : test1.c
gcc -c test1.c -o test1.o
test2.o : test2.c
gcc -c test2.c -o test2.o
test.bin : test1.o test2.o
gcc test1.o test2.o -o test.bin
all : test.bin
@echo "compile done"
当执行make all
时,make 首先发现 all 依赖于 test.bin,寻找 test.bin 不存在,于是便去寻找 test.bin的依赖文件 test1.o test2.o,同样不存在,于是寻找能生成 test1.o 和 test2.o 规则。找到后执行相应规则,有了test1.o 和 test2.o 再返回生成 test.bin,最后在执行生成 all 的规则。
自定义变量与系统变量
makefile中定义变量基本格式:
变量名=值 # 多个值之间用空格分开,值仅仅支持字符串类型。即使数字也会当作字符串处理
eg:
objfiles = test1.o test2.o
test.bin: $(objfiles)
除了自定义的变量,make也定义了很多系统变量
变量名 | 描述 |
---|---|
AS | 汇编语言编译器,默认是 as |
CC | C语言编译器,默认是 gcc |
CXX | C++语言编译器,默认 g++ |
RM | 删除命令,默认是 rm -f |
ASFLAGS | 汇编语言编译器参数,无默认值 |
CFLAGS | C语言编译器参数,无默认值 |
CXXFLAGS | C++编译器参数,无默认值 |
LDFLAGS | 链接器参数,无默认值 |
隐含规则
对于一些使用频率非常高的规则,make 把它们当成是默认的,不需要显式地写出来,当用户未在 makefile 中显示定义规则时,将默认使用隐含规则进行推导。
隐含规则只限于那些编译过程中基本固定的依赖关系,比如C语言代码文件扩展名为.c,编译生成的目标文件扩展名是.o。并且若想通过隐含规则自动推导生成目标,存在于文件系统上的文件,除扩展名之外的文件名部分必须相同。
自动化变量
为了方便,make 还支持一种自动化变量:
自动化变量 | 描述 |
---|---|
$@ | 表示规则中目标文件名集合 |
$< | 表示规则中依赖文件中的第1个文件 |
$^ | 表示规则中所有依赖文件的集合 |
$? | 表示规则中所有比目标文件mtime更新的依赖文件的集合 |
test1.o : test1.c
gcc -c test1.c -o test1.o
test2.o : test2.c
gcc -c test2.c -o test2.o
objfiles = test1.o test2.o
test.bin : $(objfiles)
gcc $^ -o $@
all : test.bin
@echo "compile done"
再方便的就是利用正则表达式匹配,%.o 表示多有以 .o 结尾的文件,make 会拿这个字符串模式去文件系统上查找文件,默认当前路径。
%.o : %.c # test1.c test2.c 都在当前目录。
gcc -c $^ -o $@
objfiles = test1.o test2.o
test.bin : $(objfiles)
gcc $^ -o $@
all : test.bin
@echo "compile done"
makefile 中执行shell命令
SVN_VERSION:=$(shell svnversion)
COMP_DATE:=$(shell date)
vim 编辑makefile
vim设置 tab 键为 4 个空格,编辑 makefile 时 可以先敲 ctrl-v 组合键,再敲 tab键,这样就不会被转换成空格了。
makefile中常用函数
wildcard
扩展通配符notdir
去除目录信息,只保留文件名。-
patsubst
替换通配符# tree . ├── a │ ├── a2.cpp │ └── a.cpp ├── b │ ├── b2.cpp │ └── b.cpp └── Makefile # cat Makefile src=$(wildcard ./a/*.cpp ./b/*.cpp) dir=$(notdir $(src)) obj=$(patsubst %.cpp,%.o,$(dir)) all: @echo $(src) @echo $(dir) @echo $(obj)
输出:
./a/a2.cpp ./a/a.cpp ./b/b2.cpp ./b/b.cpp a2.cpp a.cpp b2.cpp b.cpp a2.o a.o b2.o b.o
-
foreach
$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数
<list>
中的单词逐一取出放到参数<var>
所指定的变量中,然后再执行<text>
所包含的表达式。
每一次<text>
会返回一个字符串,循环过程中,<text>
的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>
所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。# cat Makefile SRC_PATH=./a/ ./b/ SRC_FILE=$(foreach SUB_DIR,$(SRC_PATH),$(wildcard $(SUB_DIR)*.cpp)) ALL_FILE=$(notdir $(SRC_FILE)) OBJ_FILE=$(patsubst %.cpp,%.o,$(ALL_FILE)) info: @echo $(SRC_FILE) @echo $(ALL_FILE) @echo $(OBJ_FILE
输出
./a/a2.cpp ./a/a.cpp ./b/b2.cpp ./b/b.cpp a2.cpp a.cpp b2.cpp b.cpp a2.o a.o b2.o b.o
一个简单的Makefile模板
目录规划
# tree
.
├── log
│ ├── AsyncLog.cpp
│ └── AsyncLog.h
├── main.cpp
├── Makefile
├── net
│ ├── Acceptor.cpp
│ ├── Acceptor.h
│ ├── Buffer.cpp
│ ├── Buffer.h
...
Makefile
CXX=g++
COMP_DATA:=$(shell date)
OUT_PATH=./bin/
OBJ_PATH=./obj/
DEBUG_PATH=debug/
RELEASE_PATH=release/
OUT_NAME=main
SRC_PATH=./ ./net/ ./log/
VPATH = ./ : ./net/ : ./log/
SRC_FILE=$(foreach SUB_DIR,$(SRC_PATH),$(wildcard $(SUB_DIR)*.cpp))
ALL_FILE=$(notdir $(SRC_FILE))
OBJ_FILE=$(patsubst %.cpp,%.o,$(ALL_FILE))
OBJ_DEBUG_FILE=$(addprefix $(OBJ_PATH)$(DEBUG_PATH),$(notdir $(OBJ_FILE)))
OUT_DEBUG_FILE=$(OUT_PATH)$(DEBUG_PATH)$(OUT_NAME)
DEBUG_CXXFLAGS=-g -Wall -Wno-conversion-null -Wno-format-security -Werror -DCOMP_DATE='"$(COMP_DATA)"'
debug: pre_debug $(OUT_DEBUG_FILE)
pre_debug:
-$(shell mkdir $(OBJ_PATH) -p)
-$(shell mkdir $(OBJ_PATH)$(DEBUG_PATH) -p)
-$(shell mkdir $(OUT_PATH) -p)
-$(shell mkdir $(OUT_PATH)$(DEBUG_PATH) -p)
$(OUT_DEBUG_FILE) : $(OBJ_DEBUG_FILE)
$(CXX) $(DEBUG_CXXFLAGS) $(addprefix $(OBJ_PATH)$(DEBUG_PATH), $(notdir $^)) -o $@ -lpthread
$(OBJ_PATH)$(DEBUG_PATH)%.o : %.cpp
$(CXX) -c $(DEBUG_CXXFLAGS) $< -o $@
clean:
@echo "make clean"
-$(shell rm $(OBJ_PATH) -rf )
-$(shell rm $(OUT_PATH)$(DEBUG_PATH)$(OUT_NAME) -f)
-$(shell rm $(OUT_PATH)$(RELEASE_PATH)$(OUT_NAME) -f)
info:
$(info SRC_FILE: $(SRC_FILE))
$(info ALL_FILE: $(ALL_FILE))
$(info OBJ_FILE: $(OBJ_FILE))
$(info OBJ_DEBUG_FILE: $(OBJ_DEBUG_FILE))
$(info OUT_DEBUG_FILE: $(OUT_DEBUG_FILE))