1.背景介绍
linux中C语言可执行文件a.out的由来如下:
- test.c经过预编译到test.i
- test.i经过编译到test.s
- test.s经过汇编到test.o
- test.o经过链接到a.out
但是对于一个大型工程,每次生成新的可执行文件都要重新对每个文件(或者对修改了的文件)进行编译、链接等操作的工作量太大了。
因此提出就有了make/makefile,它是一个工程文件的编译链接规则,规则写好之后,只需要一个make就能生成可执行文件。
2.语法
2.1 语法格式
all: 目标文件1 目标文件2
目标文件1: 依赖文件1, 依赖文件2...
<tab>命令1
<tab>命令2...
目标文件2: 依赖文件1, 依赖文件2...
<tab>命令1
<tab>命令2...
- 目标文件即要生成的文件,一半情况下makefile中的第一个目标文件为最终目标
- 依赖文件:该文件由哪些文件生成
- 命令:通过执行命令来由依赖文件生成目标文件。这里默认执行命令时会在终端输出命令的内容。如果在命令前加"@"符号,则终端隐藏命令,但是返回到标准输出的内容不会隐藏。
- all:makefile文件默认只生成一个目标文件即完成编译。如果想要生成多个目标文件,则需要all并放在第一行列出所有的生成文件。
2.2 变量
2.2.1 自定义变量
变量定义:
- 递归展开:VAR= var1 var2…多个值中间用空格隔开
- 直接赋值:VAR:=var。
赋值操作:
- “=”:使用"="进行赋值,变量的值是整个Makefile中最后被指定的值,例如:
VAR_A = A
VAR_B = $(VAR_A) B
VAR_A = AA
最后VIR_B的值为AA B而不是A B
- “:=”:表示直接赋值,赋予当前位置的值
VAR_A := A
VAR_B := $(VAR_A) B
VAR_A := AA
最后VIR_B的值为A B
- “?:”:表示如果该变量没有被赋值,则将后面的值赋给它
VAR ?= value
VAR的值为value
VAR := value1
VAR ?= value2
VAR的值为value1
- “+=”:类似C语言,将右边的变量追加到左边的变量中。
“$“符号表示取变量的值,当变量名多于一个字符时,需要使用”()”
2.2.2 自动变量
- “$^”: 表示所有的依赖文件,并以空格分开
- “$@”: 表示生成的目标文件
- “$<”: 代表第一个依赖文件
- “$?”: 所有时间戳比目标文件晚的依赖文件,并以空格分开
2.2.3 预定义变量
- CC:C编译器的名称,默认值为cc
- RM:文件删除程序的名称,默认值为rm -f
- CFLAGES:C编译器(gcc)的选项,默认无默认值,如-Wall、-g、-o
- AR:库文件维护程序的名称,默认值为ar
- CPP:C预编译器的名称,默认值为$(CC) -E
- CPPFLAGS:C预编译器的选项,无默认值
- CXXFLAGS:C++编译器(g++)的选项,无默认值
2.2.4 隐含变量
- “%.o”: 任意的.o文件
- “*.o”: 所有的.o文件
2.3 函数
2.3.1 名称处理函数
- wildcard
$(wildcard <pattern...>)
获取指定格式的文件列表
示例:
$(wildcard *.cpp test/*.cpp)
代表当前目录下所有的.cpp文件和test目录下所有的.cpp文件。
- dir
$(dir <names...>)
获取文件所在目录,本质上是获取最后一个"/“以前的内容,如果没有”/“,返回”./"
示例:
$(dir src/foo.c sum.txt)
返回"src/ ./"
- notdir
$(notdir <names...>)
获取一个文件路径的非目录不放呢,即获得文件名。本质上是获取最后一个反斜杠"/"之后的内容,如果没有反斜杠,则直接返回本身
示例:
$(notdir src/foo.c sum.txt)
返回"foo.c sum.txt"
- suffix
$(suffix <names...>)
获取文件的后缀,如果没有后缀,则返回空字符。本质是获取最后一个"."后面的内容
示例:
$(suffix src/foo.c test.c abc)
返回".c .c "
- basename
$(basename <names...>)
去除文件后缀
示例:
$(basename src/foo.c test.c abc)
返回"src/foo test abc"
2.3.2 字符串替换与分析函数
- subst
$(subst <src>,<dst>,<text>)
将<text>中的字符<src>替换为<dst>
示例:
$(subst aa,AA,aabbaa aAfd)
返回"AAbbAA aAfd"
- patsubst
$(patsubst <src_pattern>,<dst_pattern>,<text>)
使用目标字符格式替换源字符格式,常常搭配%使用,如果<src_pattern>和<dst_pattern>都包括%,那么此时%表示的字符内容是一样的。
示例:
$(patsubst %.cpp,%.o,a.cpp b.cpp)
返回"a.o b.o"
-
$(C_SOURCES:.c=.o)
将C_SOURCES变量中所有的.c替换成.o -
strip
去掉字符串的开头和结尾的空格
示例:
$(strip, a.cpp b.cpp)
- findstring
在某个字符串中查找指定字符串
示例:
$(findstring a,a b c)
- filter
保留指定格式的字符串
sources := a.c b.c c.c d.h
result := $(filter %.c %.s,$(sources))
返回"a.c b.c c.c"
-
filter-out
去除指定格式的字符串 -
addprefix
为字符串添加字符串
OBJ_FILES = main.o a.o b.o
PREFIXED_OBJ_FILES = $(addprefix obj/,$(OBJ_FILES))
返回"obj/main.o obj/a.o obj/b.o"
2.3.3 控制函数
- info
向标准输出打印提示信息
$(info some debug info)
终端输出"some debug info"
-
warning
向标准输出打印,用于输出警告信息,make继续执行 -
error
向标准错误输出打印文本,make停止执行
2.3.4 其他函数
- foreach
$(foreach <var>,<list>,<text>)
将<list>中的参数逐一取出放到<var>中,然后执行<text>中的表达式
- 循环执行中:每执行一次循环都会返回一个字符串,foreach循环会将返回的字符串汇总,不同字符串通过空格分隔
- 循环执行结束:当整个循环结束的时候,返回汇总的字符串
示例:
name := a b c d
files := $(foreach n,$(name),$(n).o)
返回"a.o b.o c.o d.o"
- call
$(call <expression>,<parm1>,<parm2>,...)
调用自定义的函数或者表达式,也可以传参调用
- shell
执行操作系统的shell命令,返回的是命令行命令执行的结果
示例:
$(shell ps ajx | grep text)
- eval
$(eval <text>)
可以将<text>中的内容作为makefile的一部分,然后按照makefile的语法解析这些内容,无返回值
2.4 伪目标
伪目标的格式:
.PHONY:目标
目标:依赖
<tab>命令
伪目标与常规目标的区别(无.PHONY声明):
- 常规目标会检查目标文件的修改时间与依赖文件的修改时间,如果目标文件的修改时间晚于依赖文件,则不用执行命令;反之再执行命令。
- 伪目标无论何时都会执行命令
伪目标用来定义ALL的作用:
- 避免与目标文件同名导致构建失败或者不小心touch了目标文件使得本应该重新编译链接的没有反应,提升鲁棒性
- 大多数项目将ALL声明为伪目标,遵循社区约定
还用来定义无依赖对象的伪目标,例如:
.PHONY:clean
clean:rm *.o *.out
调用时要在命令行输入:“make clean”
3.编写方式
假定有如下a.c b.c c.c main.c 四个文件,将这四个文件编译链接形成一个目标文件output
3.1 Makefile+直接编译链接
output:a.c b.c c.c main.cgcc a.c b.c c.c main.c -o output
3.2 Makefile+编译+链接
output:a.o b.o c.o main.ogcc a.o b.o c.o main.o -o outputa.o:a.cgcc -c a.c a.o
b.o:b.cgcc -c b.c b.o
c.o:c.cgcc -c c.c c.o
main.o:main.cgcc -c main.c main.o
3.3 Makefile+变量
SRC = a.o b.o c.o main.o
TARGET = output
$(TARGET):$(SRC)$(CC) $^ -o $@a.o:a.c$(CC) $^ -o $@
b.o:b.c$(CC) $^ -o $@
c.o:c.c$(CC) $^ -o $@
main.o:main.c$(CC) $^ -o $@
3.4 Makefile+模式匹配
SRC = a.o b.o c.o main.o
TARGET = output
$(TARGET):$(SRC)$(CC) $^ -o $@%.o:%.c$(CC) $< -o $@
3.5 Makefile+函数
a.h和a.cpp和main.cpp文件生成一个项目的makefile文件如下:
CXX = g++
CXXFLAGS = -Wall -std=c++17SRCS = a.cpp main.cpp
OBJS = $(SRCS:.cpp=.o)
TARGET = outputall = $(TARGET)$(TARGET): $(OBJS)$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)%.o:%.cpp$(CXX) $(CXXFLAGS) -c $< -o $@clean:rm -f $(OBJS) $(TARGET).PHONY: all clean
问题:这里的.c文件的依赖文件.h为什么不写:
这里这么写是没有问题的,它会自动检测.c的.h依赖文件。但是会出现一个问题,如果是这里修改了.h文件,make是检测不出来依赖文件改变的,它只会看.c没变,然后就不会重新编译。因此每次修改之后都需要make clean && make重新生成。
所以可以改进一下,来可以显示生成依赖关系文件,然后-include到makefile文件中如下,这时make时候就会检测到.c对.h的依赖关系,会进行时间检测是否重新编译:
CXX = g++
CXXFLAGS = -Wall -std=c++17 -MMD -MP # 启用依赖生成
SRCS = main.cpp utils.cpp
OBJS = $(SRCS:.cpp=.o)
DEPFILES = $(SRCS:.cpp=.d)
TARGET = appall: $(TARGET)$(TARGET): $(OBJS)$(CXX) $(CXXFLAGS) -o $@ $(OBJS)%.o: %.cpp$(CXX) $(CXXFLAGS) -c $< -o $@ #因为有MMD和MP参数所以会在执行这个的时候同时生成对应.d文件-include $(DEPFILES) # 包含依赖关系clean:rm -f $(OBJS) $(TARGET) $(DEPFILES).PHONY: all clean