该篇文章为摘抄,是对原作者系列文章的总汇加上标注。
Makefile入门(超详细一文读懂)_我的小卷呀的博客-CSDN博客【 Linux Makefile】Linux 中 Makefile 的简单使用_猪猪传奇的博客-CSDN博客
1、Makefile编译过程
Makefile文件中的命令有一定规范,一旦该文件编写好以后在Linux命令行中执行一条make命令即可自动编译整个工程。不同厂家的make可能会稍有不同,并且语法上也有区别,不过基本思想都差不多,主要还是落在目标依赖上,最广泛使用的是GNUmake。
2、语法规则
目标 ... : 依赖 ...
命令1
命令2
. . .
Makefile并不会关心命令是如何执行的,仅仅只是会去执行所有定义的命令,和我们平时直接输入命令行是一样的效果。
1、目标即要生成的文件。如果目标文件的更新时间晚于依赖文件更新时间,则说明依赖文件没有改动,目标文件不需要重新编译。否则会进行重新编译并更新目标文件。
2、默认情况下Makefile的第一个目标为终极目标。
3、依赖:即目标文件由哪些文件生成。
4、命令:即通过执行命令由依赖文件生成目标文件。注意每条命令之前必须有一个tab保持缩进,这是语法要求(会有一些编辑工具默认tab为4个空格,会造成Makefile语法错误)。
5、all:Makefile文件默认只生成第一个目标文件即完成编译,但是我们可以通过all 指定所需要生成的目标文件。例如下面的例子。
all: target1 target2 target3
target1:
# 编译规则1
target2:
# 编译规则2
target3:
# 编译规则3
all被设置为第一个目标,并且target1、target2和target3被列为all的依赖。当你在命令行中运行make时,make命令会寻找并执行all目标规则,这将依次执行target1、target2和target3的编译规则。
因此,通过在Makefile中设置all作为默认目标规则,你可以简化构建过程,只需运行make命令即可执行整个编译过程,无需显式指定目标

3 、 变量
$符号表示取变量的值,当变量名多于一个字符时,使用"( )"
$符的其他用法
$^ 表示所有的依赖文件
$@ 表示生成的目标文件
$< 代表第一个依赖文件
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
ALL: hello.out
hello.out: $(OBJ)
gcc $< -o $@
$(OBJ): $(SRC)
gcc -c $< -o $@
4、变量赋值
1、"="是最普通的等号,在Makefile中容易搞错赋值等号,使用 “=”进行赋值,变量的值是整个Makefile中最后被指定的值。
VIR_A = A
VIR_B = $(VIR_A) B
VIR_A = AA
经过上面的赋值后,最后VIR_B的值是AA B,而不是A B,在make时,会把整个Makefile展开,来决定变量的值
2、“:=” 表示直接赋值,赋予当前位置的值。
VIR_A := A
VIR_B := $(VIR_A) B
VIR_A := AA
最后BIR_B的值是A B,即根据当前位置进行赋值。因此相当于“=”,“:=”才是真正意义上的直接赋值
3、“?=” 表示如果该变量没有被赋值,赋值予等号后面的值。
VIR ?= new_value
如果VIR在之前没有被赋值,那么VIR的值就为new_value。
VIR := old_value
VIR ?= new_value
这种情况下,VIR的值就是old_value
4、"+="和平时写代码的理解是一样的,表示将符号后面的值添加到前面的变量上
5、预定义变量
CC:c编译器的名称,默认值为cc。cpp c预编译器的名称默认值为$(CC) -E
CC = gcc
回显问题,Makefile中的命令都会被打印出来。如果不想打印命令部分 可以使用@去除回显
@echo "clean done!"
6、函数
预定义变量
通配符
SRC = $(wildcard ./*.c)
匹配目录下所有.c 文件,并将其赋值给SRC变量。
OBJ = $(patsubst %.c, %.o, $(SRC))
这个函数有三个参数,意思是取出SRC中的所有值,然后将.c 替换为.o 最后赋值给OBJ变量。
示例:如果目录下有很多个.c 源文件,就不需要写很多条规则语句了,而是可以像下面这样写
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
ALL: hello.out
hello.out: $(OBJ)
gcc $(OBJ) -o hello.out
$(OBJ): $(SRC)
gcc -c $(SRC) -o $(OBJ)
这里先将所有.c 文件编译为 .o 文件,这样后面更改某个 .c 文件时,其他的 .c 文件将不在编译,而只是编译有更改的 .c 文件,可以大大提高大项目中的编译速度。
1.字符串处理函数
字符串替代函数:
$(patsubst <pattern>,<replacement>,<text>)
函数说明:函数功能是查找 text 中的单词是否符合模式 pattern,如果匹配的话,则用 replacement 替换。返回值为替换后的新字符串。
$(subst <from>,<to>,<text>)
函数说明:函数的功能是把字符串中的 form 替换成 to,返回值为替换后的新字符串。
对于上面这俩函数,可以看到都有substitute string,即替代字符串,不同的是,有一个加了pattern ,也就是上一个函数可以使用通配符匹配,比较高端。
去空格函数
$(strip <string>)
函数说明:函数的功能是去掉字符串的开头和结尾的字符串,并且将其中的多个连续的空格合并成为一个空格。返回值为去掉空格后的字符串。
查找字符串函数
$(findstring <find>,<in>)
函数说明:函数的功能是查找 in 中的 find ,如果我们查找的目标字符串存在。返回值为目标字符串,如果不存在就返回空。
过滤函数
$(filter <pattern>,<text>)
函数说明:函数的功能是过滤出 text 中符合模式 pattern 的字符串,可以有多个 pattern 。返回值为过滤后的字符串。
反过滤函数
$(filter-out <pattern>,<text>)
函数说明:函数的功能是功能和 filter 函数正好相反,但是用法相同。去除符合模式 pattern 的字符串,保留符合的字符串。返回值是保留的字符串。
排序函数
$(sort <list>)
函数说明:函数的功能是将 <list> 中的单词排序(升序)。返回值为排列后的字符串。【注:sort会去除重复的字符串】
去单词函数
$(word <n>,<text>)
函数说明:函数的功能是取出函数 <text> 中的第n个单词。返回值为我们取出的第 n 个单词。
2.文件名操作函数
注意:下面的每个函数的参数字符串都会被当作或是一个系列的文件名来看待。
取目录函数
$(dir <names>)
函数说明:函数的功能是从文件名序列 names 中取出目录部分,如果没有 names 中没有 “/” ,取出的值为 “./” 。返回值为目录部分,指的是最后一个反斜杠之前的部分。如果没有反斜杠将返回“./”。
取文件函数
$(notdir <names>)
函数说明:函数的功能是从文件名序列 names 中取出非目录的部分。非目录的部分是最后一个反斜杠之后的部分。返回值为文件非目录的部分。
取后缀名函数
$(suffix <names>)
函数说明:函数的功能是从文件名序列中 names 中取出各个文件的后缀名。返回值为文件名序列 names 中的后缀序列,如果文件没有后缀名,则返回空字符串。
取前缀函数
$(basename <names>)
函数说明:函数的功能是从文件名序列 names 中取出各个文件名的前缀部分。返回值为被取出来的文件的前缀名,如果文件没有前缀名则返回空的字符串。
添加后缀名函数
$(addsuffix <suffix>,<names>)
函数说明:函数的功能是把后缀 suffix 加到 names 中的每个单词后面。返回值为添加上后缀的文件名序列。
添加前缀名函数
$(addprefix <prefix>,<names>)
函数说明:函数的功能是把前缀 prefix 加到 names 中的每个单词的前面。返回值为添加上前缀的文件名序列。
链接函数
$(join <list1>,<list2>)
函数说明:函数功能是把 list2 中的单词对应的拼接到 list1 的后面。如果 list1 的单词要比 list2的多,那么,list1 中多出来的单词将保持原样,如果 list1 中的单词要比 list2 中的单词少,那么 list2 中多出来的单词将保持原样。返回值为拼接好的字符串。
获取匹配模式文件名函数
$(wildcard PATTERN)
函数说明:函数的功能是列出当前目录下所有符合模式的 PATTERN 格式的文件名。返回值为空格分隔并且存在当前目录下的所有符合模式 PATTERN 的文件名。
上面个的基于文件的函数就这些了,下面我给出一个例子,加一些我的个人看法
3.其他常用函数
foreach函数
$(foreach <var>,<list>,<text>)
函数的功能是:把参数<list>中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行<text>所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中,<text> 所返回的每个字符串会以空格分割,最后当整个循环结束的时候,<text> 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。所以<var>最好是一个变量名,<list> 可以是一个表达式,而<text>中一般会只用 <var>这个参数来一次枚举<list>中的单词。【注意:这里的var是一个局部变量,函数结束之后,var变量会消失】
if函数
$(if <condition>,<then-part>)或(if<condition>,<then-part>,<else-part>)
可见,if 函数可以包含else部分,或者是不包含,即if函数的参数可以是两个,也可以是三个。condition参数是 if 表达式,如果其返回的是非空的字符串,那么这个表达式就相当于返回真,于是,then-part就会被计算,否则else-part会被计算。
而if函数的返回值是:如果condition为真(非空字符串),那么then-part会是整个函数的返回值。如果condition为假(空字符串),那么else-part将会是这个函数的返回值。此时如果else-part没有被定义,那么整个函数返回空字串符。所以,then-part和else-part只会有一个被计算。
call函数
$(call <expression>,<parm1>,<parm2>,<parm3>,...)
call 函数是唯一 一个可以调用参数化表达式的函数。我们可以用来写一个非常复杂的表达式,这个表达式中,我们可以定义很多的参数,然后你可以用 call 函数来向这个表达式传递参数。
当 make 执行这个函数的时候,expression参数中的变量$(1)、$(2)、$(3)等,会被参数parm1,parm2,parm3依次取代。而expression的返回值就是 call 函数的返回值。
origin函数
$(origin <variable>)
origin 函数不像其他的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的。【注意: variable 是变量的名字,不应该是引用,所以最好不要在 variable 中使用“$”字符。origin 函数会员其返回值来告诉你这个变量的“出生情况”。】
在这里插入图片描述
shell函数
$(shell <command>)
shell函数把执行操作系统命令后的输出作为函数返回。
注意: 这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。
好了看完了,那我就弄一个小例子,贴上来,上面的shell函数,我单独测试一下

7、伪目标 .PHONY
伪目标只是一个标签,clean是个伪目标没有依赖文件,只有用make来调用时才会执行
当目录下有与make 命令 同名的文件时 执行make 命令就会出现错误。
解决办法就是使用伪目标
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
ALL: hello.out
hello.out: $(OBJ)
gcc $< -o $@
$(OBJ): $(SRC)
gcc -c $< -o $@
clean:
rm -rf $(OBJ) hello.out
.PHONY: clean ALL
通常也会把ALL设置成伪目标
说一下简单操作吧,细心的可能会注意到,我之前的makefile的操作,clean都是这么写的:
在这里插入图片描述
clean:
rm -rf $(OBJ) hello.out
其实这么写是不对的,第一是影响效率,第二是有些情况下,shell命令不会被执行。
规则中 rm 命令不是创建target目标 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 ftp的文件。当工作目录下不存在以 clean 为名字的文件时,在 shell 中输入 make clean 命令,命令 rm 总会被执行 ,这也是我们期望的结果。
如果当前目录下存在文件名为 clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行
可以看到,并没有出现我们预想的情况,为了解决这个问题,需要在 Makefile 中将目标 clean 声明为伪目标。将一个目标声明称伪目标的方法是将它作为特殊的目标.PHONY的依赖,如下:
.PHONY:clean
这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。
.PHONY:clean
clean:
rm -rf *.o ftp
伪目标使用格式如上所示,要先声明,然后再定义伪目标规则。
8、其他常用功能
代码清理clean
我们可以编译一条属于自己的clean语句,来清理make命令所产生的所有文件,列如
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
ALL: hello.out
hello.out: $(OBJ)
gcc $< -o $@
$(OBJ): $(SRC)
gcc -c $< -o $@
clean:
rm -rf $(OBJ) hello.out
9、嵌套执行Makefile
在一些大工程中,会把不同模块或不同功能的源文件放在不同的目录中,我们可以在每个目录中都写一个该目录的Makefile这有利于让我们的Makefile变的更加简洁,不至于把所有东西全部写在一个Makefile中。
列如在子目录subdir目录下有个Makefile文件,来指明这个目录下文件的编译规则。外部总Makefile可以这样写
subsystem:
cd subdir && $(MAKE)
其等价于:
subsystem:
$(MAKE) -C subdir
定义$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较有利于维护。两个例子意思都是先进入"subdir"目录,然后执行make命令
我们把这个Makefile叫做总控Makefile,总控Makefile的变量可以传递到下级的Makefile中,但是不会覆盖下层Makefile中所定义的变量,除非指定了 "-e"参数。
如果传递变量到下级Makefile中,那么可以使用这样的声明
export
如果不想让某些变量传递到下级Makefile,可以使用
unexport
export variable = value
等价于
variable = value
export variable
等价于
export variable := value
等价于
variable := value
export variable
如果需要传递所有变量,那么只要一个export就行了。后面什么也不用跟,表示传递所有变量
10、指定头文件路径
一般都是通过"-I"(大写i)来指定,假设头文件在:
/home/develop/include
则可以通过-I指定:
-I/home/develop/include
将该目录添加到头文件搜索路径中
在Makefile中则可以这样写:
CFLAGS=-I/home/develop/include
然后在编译的时候,引用CFLAGS即可,如下
yourapp:*.c
gcc $(CFLAGS) -o yourapp
11、指定库文件路径
与上面指定头文件类似只不过使用的是"-L"来指定
LDFLAGS=-L/usr/lib -L/path/to/your/lib
告诉链接器要链接哪些库文件,使用"-l"(小写L)如下:
LIBS = -lpthread -liconv
12、简单的Makefile实例
目录结构

include
myinclude.h
#include <stdio.h>
void print1() ;
void print2() ;
f1.c
#include "../include/myinclude.h"
void print1()
{
printf("Message f1.c\n");
return;
}
f1文件夹里的makefile
../$(OBJS_DIR)/f1.o:f1.c
@$(CC) -c $^ -o $@
f2.c
#include "../include/myinclude.h"
void print2()
{
printf("Message f2.c\n");
return;
}
f2文件夹里的makefile
../$(OBJS_DIR)/f2.o:f2.c
@$(CC) -c $^ -o $@
main.c
#include "../include/myinclude.h"
int main(int argc, char const *argv[])
{
print1();
print2();
return 0;
}
main文件夹里的makefile
../$(OBJS_DIR)/main.o:main.c
@$(CC) -c $^ -o $@
obj
此目录用来存放相关生成的目标文件
obj文件夹中的Makefile
../$(BIN_DIR)/$(BIN) : $(OBJS)
@$(CC) $^ -o $@
主Makefile
#预定义变量
CC = gcc
#预定义编译目录
SUBDIRS = f1 \
f2 \
main \
obj
#预定义目标
OBJS = f1.o f2.o main.o
BIN = myapp
OBJS_DIR = obj
BIN_DIR = bin
#传递预定义参数
export CC OBJS BIN OBJS_DIR BIN_DIR
all:CHECK_DIR $(SUBDIRS)
CHECK_DIR:
@mkdir -p $(BIN_DIR)
$(SUBDIRS):ECHO
@make -C $@
ECHO:
@echo $(SUBDIRS)
@echo begin compile
clean:
@$(RM) $(OBJS_DIR)/*.o
@rm -rf $(BIN_DIR)
bin
此文件用来存放生成的二进制文件
13、源文件搜索路径
VPATH 和 vpath 的区别:VPATH 是变量,更具体的说是环境变量,Makefile 中的一种特殊变量,使用时需要指定文件的路径;vpath 是关键字,按照模式搜索,也可以说成是选择搜索。搜索的时候不仅需要加上文件的路径,还需要加上相应限制的条件。
通俗点说,就是VPATH是一个不具有筛选条件的指定路径方法,如果缺失了一个文件,makefile会去VPATH中的所有目录中挨个寻找对比,效率低。而vpath带了模式搜索,可以选择性的指定哪种类型的文件去哪个目录下寻找,比较方便。
VPATH = src ../header
或
VPATH = src:../header
VPATH的语法如上所示,多个路径之间可以用空格隔开,也可以用冒号( : )隔开。文件搜索的顺序为,先在当前目录下寻找,如果没找到,就按照VPATH中的书写顺序挨个寻找。先搜索src,再搜索header
注意:无论你定义了多少路径,make 执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去 VPATH 的路径中去寻找。如果当前目录下有我们要使用的文件,那么 make 就会使用我们当前目录下的文件。
当VPATH中只写了src的路径的时候,不写header的路径,这时候会报错:
思考一下,这说明什么,说明我们的源文件其实也是一个target的形式存在,他们是最基本的target,然后一层一层的向上走。虽然我们的main.c或者common.c中已经指明了ftp.h的位置,但是指明归指明,makefile不会去看你的源代码,他只是根据你的依赖,去找对应的实体文件,,也就是说源代码是对makefile透明的,源代码属于其他逻辑,独立于makefile的逻辑。所以我们还是要显式指定header的路径。
正确如下:
1) vpath PATTERN DIRECTORIES
2) vpath PATTERN
3) vpath
上面是vpath的使用方法,PATTERN是模式(选择条件),可以带有通配符,后面的DIRECTORIES是指定的搜索目录。
例:
vpath test.c ../header:src #意思是,test.c文件去header下面找
vpath %.c ../header src #意思是,所有的.c文件都去header下面找
在路径的书写上同样用空格或者冒号隔开。
vpath test.c #意思是,清除之前定义的关于test.c的搜索路径设置
vpath #意思是,清除之前设置的所有的搜索路径
对vpath的验证如下:
注意:这里的指示的路径应该是包含的我们自己写的源文件的路径,如果包含了其他第三方源代码,或者第三方链接库的话,我看其他博客是直接将这些第三方的东西写在了编译命令上,用来指示第三方的路径,而不是用VPATH或vpath
在这里插入图片描述
我从别的网站上摘下了一段如何选择使用VPATH或者vpath,看看叭:
使用什么样的搜索方法,主要是基于编译器的执行效率。使用 VPATH 的情况是前路径下的文件较少,或者是搜索的文件不能使用通配符表示,这些情况下使用VPATH最好。如果存在某个路径的文件特别的多或者是可以使用通配符表示的时候,就不建议使用 VPATH 这种方法,为什么呢?因为 VPATH 在去搜索文件的时没有限制条件,所以它回去检索这个目录下的所有文件,每一个文件都会进行对比,搜索和我们目录名相同的文件,不仅速度会很慢,而且效率会很低。我们在这种情况下就可以使用 vpath 搜索,它包含搜索条件的限制,搜索的时候只会从我们规定的条件中搜索目标,过滤掉不符合条件的文件,当然查找的时候也会比较的快。
14、其他小技巧
再说几个其他小技巧
1.@的使用:
makefile在make的时候会输出执行的指令,然后再输出结果,如果我们只想要结果,不要指令显示,那么在指令面前加上@就行了。
2. 定义变量的值为一个空格
在makefile中,等式右面是很难定义一个空格的,所以这里有一个小技巧。用一个empty变量作为起始,用注释表示结束,如下:
在这里插入图片描述
nullstring 是一个Empty变量,其中什么也没有,而space的值是一个空格。所以这里先用一个 Empty变量来标明变量的值开始了,而后面采用“#”注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。请注意这里关于“#”的使用,注释符“#”的这种特性值得我们注意。也就是以后定义变量的时候,要格外注意,注释符”#“才是一个变量定义的结束。如果注释符之前有空格的话,那些空格也会算到所定义的变量之中。
本文详细介绍了Linux下Makefile的使用,包括编译过程、语法规则、变量赋值、函数使用等内容。还讲解了伪目标、嵌套执行、指定头文件和库文件路径等常用功能,给出简单实例,对比了源文件搜索路径的VPATH和vpath,最后分享了一些使用小技巧。

1730

被折叠的 条评论
为什么被折叠?



