makefile及基础语法

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中似乎 cmakelistmakefile()使用得更多。如果是单字符变量名,则可以直接用 @ 无 需 任 何 括 号 。 这 里 需 要 注 意 : 在 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)  # 删除
	
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
makefile 实例分析 Makefile 语法分析 第一部分 VERSION = 2# 给变量VERSION赋值 PATCHLEVEL = 6# 给变量PATCHLEVEL赋值 SUBLEVEL = 22# 给变量SUBLEVEL赋值 EXTRAVERSION = .6# 给变量EXTRAVERSION赋值 NAME = Holy Dancing Manatees, Batman!# 给变量NAME赋值 # *DOCUMENTATION*# To see a list of typical targets execute "make help"# More info can be located in ./README# Comments in this file are targeted only to the developer, do not# expect to learn how to build the kernel reading this file. # Do not:# o use make's built-in rules and variables#    (this increases performance and avoid hard-to-debug behavour);# o print "Entering directory ...";MAKEFLAGS += -rR --no-print-directory# 操作符“+=”的作用是给变量(“+=”前面的MAKEFLAGS)追加值。# 如果变量(“+=”前面的MAKEFLAGS)之前没有定义过,那么,“+=”会自动变成“=”;# 如果前面有变量(“+=”前面的MAKEFLAGS)定义,那么“+=”会继承于前次操作的赋值符;# 如果前一次的是“:=”,那么“+=”会以“:=”作为其赋值符# 在执行make时的命令行选项参数被通过变量 “MAKEFLAGS”传递给子目录下的make程序。# 对于这个变量除非使用指示符“unexport”对它们进行声明,它们在整个make的执行过程中始终被自动的传递给所有的子make。# 还有个特殊变量SHELL与MAKEFLAGS一样,默认情况(没有用“unexport”声明)下在整个make的执行过程中被自动的传递给所有的子make。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值