makefile 基础学习笔记

1.makefile 的书写格式

  根据 官方的GNU Make手册,一个简单的makefile文件书写规则如下所示:

target ... : prerequisites ...
		recipe
		...
		...

  target: 可以是可执行文件(executable files)或者是目标文件(object fles)

  prerequisites: prerequisites 直译为先决条件,再此表示为一个用作输入来生成 target的文档,通常情况下,一项target依赖于多个输入文档。

  recipe: recipe是make真正执行的动作,它可以是一个命令,也可以是多个命令,这些命令可以占据一行或多行。整的来说,prerequisites中如果有一个或多个的文件比target文件要新的话,recipe所定义的命令就会被执行。


2.一步一步书写makefile

  第一部分的描述比较抽象,下面通过实例说明,例子取自前一篇博客:gcc 基本操作(带例子)
为了便于理解文件间的关系,再此再一次贴工程的结构树
在这里插入图片描述

图1 工程结构树

(1)第一版makefile

  根据前一篇文章,若是我们要编译链接多个文件生成可执行文件,需要使用如下命令:
    g++ -c ./files1/Get_Finalscore.cpp -o ./files1/Get_Finalscore.o -I includes1 -I base
    g++ -c ./files2/Get_Meanscore.cpp -o ./files2/Get_Meanscore.o -I includes2 -I base
    g++ -c ./my_lib/Who_Passed.cpp -o ./my_lib/Who_Passed.o -I my_lib -I base
    g++ main.cpp -o main -I includes1 -I includes2 -I base ./files1/Get_Finalscore.o ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o

  那么根据makefile的书写规则,将其写成makefile格式为(注意makefile的命令要以Tab键开头)

#生成可执行文件main
main:main.o ./files1/Get_Finalscore.o ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o
	g++ -o main main.o ./files1/Get_Finalscore.o ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o

#生成目标文件main.o
main.o: main.cpp ./base/base.h ./includes1/Get_Finalscore.h ./includes2/Get_Meanscore.h ./my_lib/Who_Passed.h
	g++ -c main.cpp

#在./files1/文件路径下生成Get-Finalscore.o
./files1/Get-Finalscore.o:./files1/Get_Finalscore.cpp ./base/base.h ./includes1/Get_Finalscore.h
	g++ -c ./files1/Get_Finalscore.cpp -o ./files1/Get_Finalscore.o

#在./files2/文件路径下生成Get_Meanscore.o
./files2/Get_Meanscore.o:./files2/Get_Meanscore.cpp ./base/base.h ./includes2/Get_Meanscore.h
	g++ -c ./files2/Get_Meanscore.cpp -o ./files2/Get_Meanscore.o

#在./my_lib/文件路径下生成Who_Passed.o
./my_lib/Who_Passed.o:./my_lib/Who_Passed.cpp ./base/base.h ./my_lib/Who_Passed.h
	g++ -c ./my_lib/Who_Passed.cpp -o ./my_lib/Who_Passed.o

  这样我们就得到了第一版makefile了接着,我们在makefile文件的同路径下,在命令行输入:make 命令,然后再运行可执行文件main,结果如下图所示,可以看到结果输出和我们上一篇使用一个一个的命令敲出来的结果相同。
在这里插入图片描述

图2 第一版makefile运行截图

(2)make的执行过程

  make命令具体的执行过程为:make会在当前路径下寻找名为*“makefile”* 或 “Makefile” 的文件,找到了之后,它会找到文件中的第一个目标文件,也就是前面我们提到的(target),在本例子中便是main,如果main文件不存在或是生成main所依赖的*.o文件生成的时间比main文件的时间更新,那么它就会执行下面的命令来生成main*:g++ -o main main.o ./files1/Get_Finalscore.o ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o ,要是这些依赖文件(如 main.oGet_Finalscore.o )也不存在呢,那它会在makefile文件中找到目标为这些依赖文件的依赖,找到了之后在执命令生成这些依赖文件,如上例所示:g++ -c main.cpp、**g++ -c ./files1/Get_Finalscore.cpp -o ./files1/Get_Finalscore.o等命令,由于这些.o文件所依赖的源文件肯定已经写好了,那生成这些文件肯定不成问题。正如图2所示,一步一步先生成.o文件,然后最后一步在执行命令:g++ -o main main.o ./files1/Get_Finalscore.o ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o 生成最终的目标文件main

(3)第二版makefile

  看到这里,肯定有朋友就要说了,你这也没节省多少时间啊,不就是把原来一行一行的命令放到一个文件中,然后一次性执行完吗。那么现在我们就开始“化简”工作,这里我们主要依靠五个东西:
    ①make自动推导文件以及文件依赖关系后面的命令的特性
    ②makefile中的“变量”
    ③makefile中的通配符
    ④makefile中的函数
    ⑤shell下的命令

  根据第②条:makefile中的“变量”,我对第一版的makefile做了一点小小的修改,代码如下所示:

  1 ##makefile v2                                                                                                                                             
  2 
  3 CC = g++
  4 
  5 TARGET = main
  6 
  7 OBJS = main.o ./files1/Get_Finalscore.o \
  8        ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o
  9 
 10 INC =./base/base.h ./includes1/Get_Finalscore.h \
 11      ./includes2/Get_Meanscore.h \
 12      ./my_lib/Who_Passed.h
 13 
 14 $(TARGET):$(OBJS)
 15     $(CC) -o $(TARGET) $(OBJS)
 16 
 17 main.o:main.cpp $(INC)
 18     $(CC) -c main.cpp
 19 
 20 ./files1/Get_Finalscore.o:./files1/Get_Finalscore.cpp $(INC)
 21     $(CC) -c ./files1/Get_Finalscore.cpp -o ./files1/Get_Finalscore.o
 22 
 23 ./files2/Get_Meanscore.o:./files2/Get_Meanscore.cpp $(INC)
 24     $(CC) -c ./files2/Get_Meanscore.cpp -o ./files2/Get_Meanscore.o
 25 
 26 ./my_lib/Who_Passed.o:./my_lib/Who_Passed.cpp $(INC)
 27     $(CC) -c ./my_lib/Who_Passed.cpp -o ./my_lib/Who_Passed.o

  首先解释一下为什么这里的“变量”打了引号,因为这里的“变量”不同于C语言等语言的变量,个人认为这里的“变量”的作用类似于C语言的宏,GNU官方的make使用手册中对make“变量”的解释为:A variable is a name defned in a makefle to represent a string of text,体现的是字符的替换,但是在下面的描述中,我仍然使用“变量”一词描述。
  观察第一版makefile可以发现:那一堆 .o文件和 .h出现了好几次,那么我们可以将其用变量表示,如上一段代码的第7行和第10行,makefile里的变量赋值也是使用等号,即用等号左边的名字代替右边,然后再用到的地方使用 $外加后面的() 将变量括起来,如上一段代码的第14行,使用 $(TARGET)代替main。
  这里的五大法宝才使用到了一项,接下来我们继续“化简”,再次进行改进,代码如下:

  1##makefile v2                                                                                                                                             
  2 
  3 CC = g++
  4 
  5 TARGET = main
  6 
  7 OBJS = main.o ./files1/Get_Finalscore.o \
  8        ./files2/Get_Meanscore.o ./my_lib/Who_Passed.o
  9 
 10 INC =./base/base.h ./includes1/Get_Finalscore.h \
 11      ./includes2/Get_Meanscore.h \
 12      ./my_lib/Who_Passed.h
 13 
 14 $(TARGET):$(OBJS)
 15     $(CC) -o $(TARGET) $(OBJS)
 16 
 17 %.o : %.cpp $(INC)
 18     $(CC) -c $< -o $@

  对比第一次修改的makefile,这个是不是简洁了很多,这里主要的修改就是上个文件的1727行修改成了这里的1718行。17行的 %.o : %.c 是一种模式规则,% 表示长度任意的非空字符串(即用 %.o 表示了main.oGet_Finalscore.o等)。同理用 %.cpp 表示所有的cpp文件。18行这里用到的 $<$ @ 是makefile的自动化变量,$< 表示当前的依赖文件名,$ @ 表示当前的目标文件名,如例子中最开始执行的时候,$< 表示的是main.cpp, $ @ 表示的是main.o ,下一次 $<files1/Get_Finalscore.cpp$ @ 表示的是files1/Get_Finalscore.o

(3)第三版makefile

  第二版的makefile文件已经非常简洁了,但是有的时候我们的源文件数量比较多的时候,比如用过STM32库函数开发的朋友应该知道,那些库文件又多,库文件名又长,不细心的话比较容易打错;还有,第二版的makefile生成的 .o文件分别是在各自源文件的位置,看起来杂乱无章,不利于管理,若是能够把这些 .o文件存到一个地方就好了。这个时候就要用到makefile的函数和shell下的命令这两大法宝了。

  1 ##makefile v3                                                                                                                                             
  2 TARGET = main #文件中的第一个目标文件,并把这个文件作为最终的目标文件
  3 CC = g++
  4 
  5 # //! 注意每行后面一定不能有空格
  6 PRO_DIR     = $(shell pwd)#
  7 BUILD_DIR   = build
  8 OBJ_DIR     = $(BUILD_DIR)/obj#以$()方式使用定义的变量
  9 DEPS_DIR    = $(BUILD_DIR)/deps
 10 
 11 INC_DIR =   -I./includes1 \
 12             -I./includes2 \
 13             -I./base\
 14             -I./my_lib
 15 
 16 CC_FLAGS := $(INC_DIR) -g
 17 
 18 DIRS := $(shell find $(PRO_DIR) -maxdepth 2 -type d) #
 19 VPATH = $(DIRS) #指定搜索路径
 20 
 21 SOURCES   = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.cpp))    #找到DIRS目录下所有的cpp文件
 22 OBJS      = $(addprefix $(OBJ_DIR)/,$(patsubst %.cpp,%.o,$(notdir $(SOURCES)))) #把前一个命令找到的cpp文件变为o文件并且添加OBJ_DIR目录
 23 DEPS      = $(addprefix $(DEPS_DIR)/, $(patsubst %.cpp,%.d,$(notdir $(SOURCES)))) #把前一个命令找到的cpp文件变为d文件并且添加DEPS_DIR目录
 24 
 25 $(TARGET):$(OBJS)
 26     $(CC) $^ $(LINK_FLAGS) -o $@ 
 27 
 28 $(OBJ_DIR)/%.o:%.cpp
 29     @if [ ! -d $(OBJ_DIR) ]; then mkdir -p $(OBJ_DIR); fi ;\
 30     $(CC) -c $(CC_FLAGS) $< -o $@ 

  第三版makefile实现了自动查找源文件,并且将生成的o.文件保存在./build/obj文件路径下的功能,下面来详细解释每一行的作用:
  第6行:项目路径,使用shell pwd 调用shell下的pwd命令,获取makefile文件所在的绝对路径
  第8,9行:第8行的变量存放 .o文件,第9行的变量存放 .d文件
  第11-14行:使用 -I参数包含头文件路径
  第16行: g++运行加的参数,-g表示生成调试信息 :== 不同的是 := 限制前面的变量不能使用后面的变量
  第18行:shell 命令,以最大深度为2层为限制,找出项目路径下的所有文件路径,比如这里我输出DIRS的结果如下图所示:(注:这里我写了个伪目标D,关于伪目标,在后面会说明)
在这里插入图片描述

图3 DIRS的值

  第19行:VPATH是nakefile的一个特殊的变量,用来指定make寻依赖文件和目标文件的路径,若是没有指定,make只会在当前路径进行寻找,若是要指定多个路径,使用冒号(:)隔开。
  第21行:用到了makefile的两个函数,找到DIRS目录下所有的cpp文件,函数将会在函数介绍章节详细说明,输出SOURCES的结果如下图所示.
在这里插入图片描述

图4 SOURCES的值

  第22行:用到了makefile的两个函数,把前一行找到的cpp文件变为o文件并且添加OBJ_DIR目录(注意,其实这些都只是带路径的文件名),输出结果如下图所示:
在这里插入图片描述

图4 OBJS的值

  第23行:与22行类似,把找到的cpp文件变为d文件并且添加DEPS_DIR目录
  第26行:生成TARGET目标文件,这里的 $^ 也是自动化变量,表示所有的依赖目标的集合。以空格分隔。
  第29行:条件判断语句,与C语言的if语句类似,判断是否存在OBJ_DIR这一路径,若是不存在则创建。

(4)最终版makefile

  第三版的makefile已经很完善了,但是有一点就是当在处理一些大型工程的时候,我们必须要清楚源文件需要包含那些头文件,这也是一个不小的工作量,这里我们可以使用 C/C++ 编译器的一个功能自动生成依赖性。然后我们再加上一些注释和清除文件的伪目标,便有了最终版的makefile,如下所示:

  1 ##makefile final                                                                                                                                             
  2 
  3 TARGET = main #文件中的第一个目标文件,并把这个文件作为最终的目标文件
  4 CC = g++
  5 
  6 # //! 注意每行后面一定不能有空格
  7 PRO_DIR     = $(shell pwd)
  8 BUILD_DIR   = build
  9 OBJ_DIR     = $(BUILD_DIR)/obj#以$()方式使用定义的变量
 10 DEPS_DIR    = $(BUILD_DIR)/deps
 11 
 12 INC_DIR =   -I./includes1 \
 13             -I./includes2 \
 14             -I./base\
 15             -I./my_lib
 16 
 17 CC_FLAGS := $(INC_DIR) -g -std=c++11 # ":=" 前面的变量不能使用后面的变量
 18 LINK_FLAGS :=-L/home/nvtu18/MyCodes/CPrimerCodes/Make_Demo/My_MakeTest/my_lib/ -ltools#lib
 19 
 20 DIRS := $(shell find $(PRO_DIR) -maxdepth 2 -type d) #
 21 VPATH = $(DIRS) #指定搜索路径
 22 
 23 #wildcard:扩展通配符函数 src=$(dir)/*.cpp 表示获取dir目录下所有的cpp文件
 24 #foreach:循环
 25 #   $(foreach <var>,<list>,<text> ):把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,
 26 #   然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个
 27 #   字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)
 28 #   将会是foreach函数的返回值
 29 #addprefix:添加固定前缀 $(addprefix fixstring,string1 string2 ...)
 30 #patsubst:替换通配符,将cpp替换为o,$(patsubst 原模式, 目标模式, 文件列表)
 31 #notdir:去掉目标的路径函数
 32 SOURCES   = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.cpp))    #找到DIRS目录下所有的cpp文件
 33 OBJS      = $(addprefix $(OBJ_DIR)/,$(patsubst %.cpp,%.o,$(notdir $(SOURCES)))) #把前一个命令找到的cpp文件变为o文件并且添加OBJ_DIR目录
 34 DEPS      = $(addprefix $(DEPS_DIR)/, $(patsubst %.cpp,%.d,$(notdir $(SOURCES)))) #把前一个命令找到的cpp文件变为d文件并且添加DEPS_DIR目录
 35 
 36 #
 37 #$^:所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
 38 #$@:表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,"$@"就是匹配于目标中模式定义的集合。
 39 #$<:依赖目标中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么"$<"将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
 40 $(TARGET):$(OBJS)
 41     $(CC) $^ $(LINK_FLAGS) -o $@ 
 42 
 43 #
 44 $(OBJ_DIR)/%.o:%.cpp
 45     @if [ ! -d $(OBJ_DIR) ]; then mkdir -p $(OBJ_DIR); fi ;\
 46     $(CC) -c $(CC_FLAGS) $< -o $@ 
 47 
 48 #  $< > $@.    g' < $@ > $@    rm -f $@. 后面的点可以省略
 49 $(DEPS_DIR)/%.d:%.cpp
 50     @if [ ! -d $(DEPS_DIR) ]; then mkdir -p $(DEPS_DIR); fi;\   
 51     set -e; rm -f $@;\
 52     $(CC) -MM $(CC_FLAGS) $< > $@.$$$$;\
 53     sed 's,\($*\)\.o[ :]*,$(OBJ_DIR)/\1.o $@ : ,g' < $@.$$$$ > $@;\
 54     rm -f $@.$$$$
 55 
 56 -include $(DEPS)
 57 
 58 .PHONY : clean
 59 clean:
 60     rm -rf $(BUILD_DIR) $(TARGET)

  在这里解释一下上面的部分代码,首先是第18行的LINK_FLAGS,还有印象吗,这就是前一篇博客的链接库函数的命令,这里的库函数由./my-lib/文件夹下的makefile生成,makefile源码如下所示:

 1 #2022-01-20                                                                                                                                               
  2 #生成静态库
  3 
  4 TARGET = libtools.a
  5 CC = g++
  6 AR = ar -crv
  7 
  8 
  9 PRO_DIR = ~/MyCodes/CPrimerCodes/Make_Demo/My_MakeTest
 10 LIB_DIR = $(shell pwd)
 11 SRC_DIR = $(shell find $(LIB_DIR) $(PRO_DIR)/base -name "*.cpp")
 12 
 13 INC_DIR = -I./my_lib\
 14           -I./base
 15 
 16 OBJS = $(SRC_DIR:.cpp=.o)
 17 
 18 $(TARGET):$(OBJS)
 19     $(AR) $(LIB_DIR)/$(TARGET) $(OBJS)
 20     @echo 
 21 
 22 $(OBJS):$(SRC_DIR)
 23     $(CC) -c $(SRC_DIR) -o $(OBJS)
 24 
 25 .PHONY : clean
 26 clean:
 27     rm -rf  $(OBJS) $(TARGET)

  然后是代码的51行到54行,这个规则的意思是,所有的.d文件都依赖于.c文件,第52行表示给每一个依赖文件$<(.cpp文件)生成依赖文件(.d文件),而 $@ 表示的就是 %.d文件,如例子里的main.cpp,则 % 就是main$$$$ 表示的是个进程数,则52行的输出类似于main.d.1234,第53行的sed命令表示在 $@.$$$$ 临时文件中,用 $(OBJ_DIR)/\1.o $@ :全局替换($*).o[ :]*,然后将替换完成的文件保存为 $@ 文件(既.d文件),$* 表示的是target的除去了suffix后的filename,也就是 %.d: %.c当中的 %部分。假设52行生成的main.d.1234文件里面的内容如下图所示:
在这里插入图片描述

图5 g++ -MM main.cpp的输出

则经过53行命令后,上面的第一行变成了:

main.o main.d: main.cpp \

理解的还是不算透彻。
  然后是代码的第59行与60行,这里的clean便是一个伪目标,伪目标不是生成一个文件,而是一个标签,我们只有显式的指明这个标签才能使标签下面的命令生效,比如在命令行输入:make clean,则make会执行clean表前下的语句,也就是清除build文件就夹和main文件。
至于第58行,则是为了避免标签和文件重名的这种情况,使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”。

3.makefile常用函数介绍

  makefile的函数语法如下所示:

$(<function> <arguments>)

<function>表示的是函数名,<arguments>表示的是参数,不同参数之间采用逗号(,)分隔,函数名和参数之间使用空格( )分隔。

(1)subst函数

$(subst from,to,text)
函数名称:字符串替换函数
函数功能:把字串 text 中的 from 字符串替换成 to 。
函数返回值:函数返回被替换过后的字符串。

使用示例:
ORI = Hello world
SUB = $(subst H,h,$(ORI))
P:
	@echo $(ORI)
	@echo $(SUB)	

  输出结果如图6所示:
在这里插入图片描述

图6 subst函数输出

(2)patsubst函数

$(patsubst pattern,replacement,text)
函数名称:模式字符串替换函数
函数功能:查找 text 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式
pattern ,如果匹配的话,则以 replacement 替换。这里, pattern 可以包括通配符 % ,
表示任意长度的字串。如果 replacement 中也包含 % ,那么, replacement 中的这个 % 将是
pattern 中的那个 % 所代表的字串。(可以用 \ 来转义,以 \% 来表示真实含义的 % 字符)
函数返回值:函数返回被替换过后的字符串。

使用示例:
PAT = $(patsubst %.cpp,%.o,a.cpp b.cpp)
P:
	@echo $(PAT)

  输出结果如图7所示:
在这里插入图片描述

图7 patsubst函数输出

(3)wildcard函数

$(wildcard pattern)
函数名称:扩展通配符函数
函数功能:扩展通配符
函数返回值:扩展后的通配符

使用示例:
例程如图4所示
src=$(dir)/*.cpp 则表示获取dir目录下所有的cpp文件

(4)foreach函数

$(foreach var,list,text)
函数名称:循环函数
函数功能:把参数<list>中的单词逐一取出放到参数var所指定的变量中,
然后再执行text所包含的表达式。每一次text会返回一个字符串,循环过程中,text的所返回的每个
字符串会以空格分隔,最后当整个循环结束时,text所返回的每个字符串所组成的整个字符串(以空格分隔)
将会是foreach函数的返回值
函数返回值:text所返回的每个字符串所组成的整个字符串(以空格分隔)

使用示例:
如图4所示

(5)addprefix函数

$(addprefix prefix,names...)
函数名称:增加前缀函数
函数功能:把前缀prefix加到names中的每个单词后面
函数返回值:返回加过前缀的文件名序列

使用示例:
ORI = obj/
ADP = $(addprefix $(ORI),main.cpp)
P:
	@echo $(ORI)
	@echo $(ADP)	

  输出结果如图8所示:
在这里插入图片描述

图8 addprefix函数输出

(6)notdir函数

$(notdir names...)
函数名称:取文件函数
函数功能:从文件名序列names中取出非目录部分
函数返回值:文件

使用示例:
ORI = obj/culculate.cpp
NOD = $(notdir $(ORI))
P:
	@echo $(ORI)
	@echo $(NOD)

  输出结果如图9所示:
在这里插入图片描述

图9 notdir函数输出

4.总结

  本篇博客记录了本人学习makefile基础知识的过程,通过使用上一篇的例子,一步一步对makefile进行修改,使它更加的规范和简洁,博客de1很多内容是学习了陈皓大神的《跟我一起写 Makefile》之后的总结与实验,由于缺乏实际项目的锻炼(实验中的例子比较浅显),所以有很多东西并未理解的比较好,下一篇博客,将采用在Linux上使用makefile开发STM32的方式加深印象,本人不才,必有疏漏,望指正。

5.参考

https://blog.csdn.net/haoel/article/details/2886
https://www.gnu.org/software/make/manual/make.pdf
https://blog.csdn.net/yufei_email/article/details/78575637
https://www.cnblogs.com/maxincai/p/5146338.html
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值