makefile命令和语法
(1)定义变量:变量值的本质是一个字符串,可以是文件名列表,参数烈,目录列表等。通常对于参数列表变量,写成大写方式;对于内部一般变量写成小写形式。
同时有两种定义变量方式,一种obj:=表示直接展开式变量,该变量是在变量定义时就求值,另一种obj=, obj+=, obj?=都表示递归展开式变量,这种变量是在变量被引用时才求值。
obj=代表递归赋值,obj+=代表递归追加赋值,obj?=代表条件赋值(只有该变量在之前没有被赋值的情况下才会对他赋值)
obj = main.o simple.o #创建变量
foo1 := ${aaa} #直接展开式变量,此时aaa还没定义,所以foo1值为空
foo2 = ${aaa} #递归展开式变量,在执行时才会求值,所以foo2值为hello
aaa = 16
foo3 = 4
foo3 += aaa # foo3追加
foo4 ?= foo3 # foo4之前没有赋值过,所以foo4可以等于foo3
(2)几种特殊变量:这里重点是3种单字符变量
$@ # 代表当前规则中的目标target文件名
$< # 代表当前规则中的第一个依赖文件名
#^ # 代表当前规则中的所有依赖文件名
在显示这几种特殊变量时,也需要额外特殊的方式
all: first second third
echo “\$$@ = $@” # 这里\$$@就代表字符$@
echo “$$< = $<” # 这里$$<就代表$<
echo “$$^ = $^” # 这里$$^就代表$^
(4)引用变量:采用 跟 {}跟 跟()方式等效,其中 跟 c m a k e l i s t 方 式 一 样 , 但 在 m a k e f i l e 中 似 乎 {}跟cmakelist方式一样,但在makefile中似乎 跟cmakelist方式一样,但在makefile中似乎()使用得更多。如果是单字符变量名,则可以直接用 @ 无 需 任 何 括 号 。 这 里 需 要 注 意 : 在 m a k e f i l e 中 采 用 @无需任何括号。 这里需要注意:在makefile中采用 @无需任何括号。这里需要注意:在makefile中采用()是可以的,但在shell脚本和cmakelists中 ( ) 是 非 法 的 , 只 能 用 ()是非法的,只能用 ()是非法的,只能用{},所以有时候为了便于记忆,也可以在shell脚本中尽可能用 代 替 {}代替 代替(),细节可参考[shell脚本提取变量]和[cmakelists]
(5)变量增加前缀
采用$(addprefix, str, $(var))用来给var批量添加前缀字符串str
$(addprefix, $(DIR), $(srcs)) # 把srcs文件都添加前缀路径
(6)变量去除dir前缀
采用$(notdir, $(var))用来给var批量去除路径前缀,仅保留文件名
$(notdir, $(srcs)) # 把srcs文件都去掉前缀路径
(7)变量名的快速替换方法1:
采用$(padsubst, src, dst, $(var)) 注意这个命令只是对一个变量的值进行替换,而不是对实际的文件名进行改名。功能跟下一条快速替换是一样的。
$(padsubst %.cpp, %.o, $(var)) # 把变量中所有.cpp变量替换成.o文件名
(8)变量名的快速替换方法2:
通过$(var:A=B)可以把变量名var中每个以空格分开的字符串中结尾字符A全部替换为B字符。这个方法可以用来快速把.c文件替换成.o文件。
基于这种变量名替换方法,就可以把一个写的复杂的makefile简化写法
test : main.o test.o
gcc main.o test.o -o test
main.o : main.c
gcc -c main.c -o main.o
test.o : test.c
gcc -c test.c -o test.o
简化成一个makefile标准版:从而大部分c/c++代码只需要修改前面4行就可以。
executable := test # 定义生成的可执行文件名
src := main.c test.c # 定义源文件名
obj := $(src:.c=.o) # 定义目标文件名
cc := gcc # 定义编译器类型,可以是gcc,g++...
$(executable) : $(obj) # 终极规则:生成可执行文件
$(cc) -o $@ $^
main.o : main.c
$(cc) -c $< -o $@
test.o : test.c
$(cc) -c $< -o $@
(9)命令的回显:
由于在makefile中执行任何shell指令时,不仅会输出内容,还会输出shell指令本身,也就是所谓命令回显,这种回显是可以关闭的。关闭方法1:在指令前面增加@符号,关闭方法2:执行make时带上参数-s (表示silent),关闭方法3:在makefile中增加使用没有任何依赖的特殊目标.SILENT
all:
echo “Hello world!” # 会带有命令回显
@echo “Hello world!” # 不会带有命令回显
.SILENT: # 关闭命令回显
注意:这里all是一条没有依赖的规则,只有放在最开始作为终极规则才会被执行否则不会执行。但由于没有依赖,所以也不会再执行其他规则,相当于一条孤岛规则,只能通过make指定来执行。
(10)cd命令在一行和两行的区别:
由于makefile中每一行都是独立的shell命令行,不会对下一行产生影响,所以cd命令进入的目录不会对下一行命令产生影响。为了让cd进入的目录对下一行命令产生影响,则需要把cd命令跟接下来执行的命令写在同一行,中间分号隔开,这样采用一个集成独立的shell命令。
target1:
cd ~; pwd # 这是集成独立的命令,打印的是cd进入后的目录
target2:
cd ~;
pwd # 这是两条独立的命令,打印的是cd进入之前的目录
(11)命令执行的错误处理:
如果命令执行错误,通常make就会返回非0状态,并终止make的解析过程。为了让有的命令检查后虽然出错但不要接下来的检查执行,可在命令前面加负号-,比如rm某个不存在的文件是常见错误,一般不希望他报错,如下:
clean:
-rm $(deps) # 由于用户任何时候都可能运行clean, 如果此时deps
# 还没生成,则会报错,添加一个-号,则不会报错了
(12)多目标规则和多规则目标:
- 多目标规则:是指多个target放在一起,共用一组依赖prerequisites和一组命令command。最常用的就是静态模式规则,写法是:
targets: targets_pattern: prerequisites_pattern,含义就是从多个targets中按照targets_pattern定义的筛选方式逐个取出单个target,并采用依赖模式定义的筛选方式取出依赖,分别用相同的command生成对应target。这种方式可用来简化makefile的写法。
$(obj) : %.o: %.c # 多个.o的target都在obj中预定义,用%表示提取某
# 一个.o文件作为target,对应%.c作为依赖
$(cc) -c $< -o $@ # 生成目标文件
注意:这种静态模式规则还有一种写法,就是省略target,但需要在依赖中把冒号前后的路径交代清楚。比如这里需要指明.o文件和.c文件的路径。
$(BUILD_DIR)%.o : $(SRC_DIR)%.c
$(cc) -c $< -o $@
- 多规则目标:是指同一个target出现在多条规则中,make是允许这种情况的,但只允许该目标不是文件,也就是不会重复重建文件。如果多条规则都重复重建该目标,此时make会采用最后一个规则的命令进行目标重建,同时报错。
(13)伪目标:
伪目标用来定义的目标,明确该目标是标签而不是文件,也不需要生成。且是一条孤立的目标,在make时不执行,但可以用make单独执行,一般用来把编译生成的所有文件删除,让整个项目回到最初装填,最常用的伪目标是all和clean。定义伪目标的方式是:.PHONY: target
注意:如果没有把clean定义成伪目标,一般也能执行,但是如果目录中存在一个叫clean的文件,此时该clean规则在依赖没有变化的情况下(实际上是没有依赖),该规则就不会被重新执行,也就无法清除路径了。
.PHONY: all clean # 定义2个伪目标
all:
echo “hello”
clean:
rm -rf test test.o main.o
(14)make中内嵌的函数
函数的调用方式类似与变量的引用: ’$(func_name arguments)’
$(wildcard *.c) # 获取当前目录下所有符合筛选条件的文件名(*,?通配符)
(15)项目依赖关系的分析:
a.在编译过程中,头文件加入主程序是在预处理步骤(g++ -E)把头文件插入到主文件对应位置的,这是编译器自动完成。而makefile在处理过程中,理论上并不需要把头文件也包含进去,因为cpp文件会通过include包含头文件,但带来的问题就是makefile并不知道头文件有没有更新,他只要看到依赖文件没有更新就不会重新编译,从而导致头文件的更新无法体现。
因此,我们必须把头文件也作为目标文件生成的依赖文件,否则makefile就无法检测头文件是否有更新过。
b.检测一个cpp文件需要哪些头文件比较麻烦,可通过gcc -MM指令来获取
$gcc -MM main.c # 生成的结果形式是 test.o : test.cpp aaa.h
c.为了把.h头文件包含进来,采用sinclude指令,他的作用就是增加一条规则。比如一条deps的形式是test.o:test.cpp aaa.h,也就是为了生成.o文件需要cpp和h文件作为用来,此时这条规则就通过sinclude加到makefile中。也就是说sinclude可以打开.d文件把里边内容直接转换成一条规则:
sinclude $(deps)
//等价于如下
test.o : test.cpp aaa.h
d.所以在makefile中就可以利用sinclude结合gcc -MM指令来获取头文件依赖列表,从而增加依赖规则.
sinclude $(deps) # deps的形式是test.o:test.cpp aaa.h,相当于取出deps并作为一条规则
$(deps) : %.d:%.cpp # .d文件作为目标,.cpp文件作为依赖
$(CC) -MM $< > $@
# 提取每一个.cpp文件,通过-MM检查依赖,把结果通过>写入.d文件
注意:这里采用了自定义一个额外.d文件,并通过>符号把gcc -MM的结果写入.d文件。
(16)最终的makefile模板
如下是我写的一个完整makefile模板,可以设置对应生成路径,可自动清理安装痕迹。
对应makefile项目源码在EX1_CPrimer/1-3-makefile
注意:注释不能写在路径变量后边,否则中间的空格会被认为是路径的一部分。
# 定义源文件路径,可以为./或者为./src
# 目标文件夹路径,可以为./或者为./build
# 定义可执行文件路径,可以为./或者./build
# 头文件路径定义-I
# 库文件路径定义-l和-L
TARGET := test
SRC_DIR = ./src
BUILD_DIR := ./build
EXEC_DIR = ./
INC := -I./
LIBS := -lpthread -L./
CC := g++
CFLAGS := -std=c++11
RM := rm -rf
srcs := $(foreach dir, $(SRC_DIR), $(wildcard $(dir)/*.cpp)) #循环搜索src dir获取所有cpp文件
objs := $(addprefix $(BUILD_DIR)/, $(patsubst %.cpp, %.o, $(notdir $(srcs)))) # 先srcs文件去除路径保留文件名
deps := $(addprefix $(BUILD_DIR)/, $(patsubst %.cpp, %.d, $(notdir $(srcs)))) # 然后替换成.o,添加前缀得完整路径
exec := $(EXEC_DIR)/$(TARGET)
.PHONY : all clean # 定义2条伪指令,避免被作为target
.SILENT: # 禁用命令回显
all : $(exec) # 把伪指令用可执行文件做依赖,从而把可执行文件串联到编译过程
# @echo "srcs: $(srcs)"
# @echo "objs: $(objs)"
# @echo "deps: $(deps)"
# @echo "exec: $(exec)"
@echo "Successfully compiled." # exec依赖生成以后就会执行该规则,打印输出,所以这是最后一步。
$(exec) : $(objs)
if [ ! -d $(BUILD_DIR) ]; then mkdir -p $(BUILD_DIR); fi; # 检查build文件是否存在,不存在则创建
$(CC) $^ -o $@ $(INC) $(LIB) # 基于所有依赖($^),生成目标($@)
$(BUILD_DIR)/%.o : $(SRC_DIR)/%.cpp # 静态模式筛选多依赖和多目标
if [ ! -d $(BUILD_DIR) ]; then mkdir -p $(BUILD_DIR); fi; # 检查build文件是否存在,不存在则创建
$(CC) -c $< -o $@ $(CFLAGS) $(INC) # 基于第一个依赖.cpp($<),生成目标.o($@)
sinclude $(deps) # deps的形式是test.o:test.cpp aaa.h,相当于取出deps并作为一条规则
$(BUILD_DIR)/%.d : $(SRC_DIR)/%.cpp # 依赖文件
if [ ! -d $(BUILD_DIR) ]; then mkdir -p $(BUILD_DIR); fi; # 检查build文件是否存在,不存在则创建
$(CC) -MM $< > $@ # 依次提取一个.cpp文件,通过-MM检查依赖,把结果通过>写入.d文件
clean: # 孤立目标,用来清除安装痕迹
$(RM) $(exec) $(objs) $(deps) $(BUILD_DIR) # 删除