Linux 开发 | 学习 Makefile
What is GNU Make
Make 是控制工程中通过源码生成可执行文件和其他相关文件的工具。Make 通过 Makefile 获取如何编译、链接和安装清理工程的信息。主要参考 GNU Make Manual。获取详细信息请直接阅读手册。
本文用Linux服务器下编写C程序来讲解Makefile,主要实现两个目标:
- 自己能写简单的Makefile,玩转Linux编程。
- 看懂大神写的Makefile
Makefile之hello world(入门)
项目目录结构
. ├── bin # 执行码 │ └── box ├── include # 头文件 │ └── box.hpp ├── lib # 静态/动态库 ├── Makefile └── src # 源码 └── box.cpp
box.hpp
class Box { public: double length; // 盒子的长度 double breadth; // 盒子的宽度 double height; // 盒子的高度 };
box.cpp
#include <iostream> #include "box.hpp" using namespace std; int main( ) { Box Box1; // 声明 Box1,类型为 Box Box Box2; // 声明 Box2,类型为 Box double volume = 0.0; // 用于存储体积 // box 1 详述 Box1.height = 5.0; Box1.length = 6.0; Box1.breadth = 7.0; // box 2 详述 Box2.height = 10.0; Box2.length = 12.0; Box2.breadth = 13.0; // box 1 的体积 volume = Box1.height * Box1.length * Box1.breadth; cout << "Box1 的体积:" << volume <<endl; // box 2 的体积 volume = Box2.height * Box2.length * Box2.breadth; cout << "Box2 的体积:" << volume <<endl; return 0; }
Makefile
hello:./src/box.cpp g++ -I ./include -c ./src/box.cpp # 预编译 g++ -o box box.o # 链接 rm -f box.o # 删除中间文件 mv box ./bin # 移动执行码
Makefile结构说明
Makefile里主要包含了五个东西:变量定义、显式规则、隐晦规则、文件指示和注释。
1、变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
2、显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。 刚才写的疑似shell脚本的Makefile全部都是显示规则。
3、隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。
4、文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样。
5、注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符。
复杂一些的Makefile
根据上面的结构说明,我们对Makefile一层一层的改写,首先是隐晦规则,告诉大家其中一种用法:
.SUFFIXES: .cpp .c
.cpp.o:
g++ ${INCL} -c $<
.c.o:
gcc ${INCL} -c $<
这个隐晦规则其实就是告诉大家,后缀为cpp的文件怎么编译成.o,后缀为c的文件怎么编译成.o。
Makefile升级
# 定义变量 INCL=-I ./include #隐含规则 .SUFFIXES: .cpp .c .cpp.o: g++ ${INCL} -c $< .c.o: gcc ${INCL} -c $< #隐含规则end #C++编译 box:./src/box.o # 隐含规则已经编译 g++ $(INCL) -c box.cpp echo "开始编译" g++ -o box box.o rm -f box.o mv box ./bin echo "编译结束" #C编译 # hello:hello.o # # 隐含规则已经编译 g++ $(INCL) -c hello.c # echo "开始编译" # gcc -o hello hello.o # rm -f hello.o # mv hello ./bin # echo "编译结束"
细心的同学会发现刚才有个“$<”,如果看的有些蒙圈,那一定要了解预定义变量,下面这些是常用的预定义变量。
$* 不包含扩展名的目标文件名称。
$+ 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件。
$< 第一个依赖文件的名称。
$? 所有的依赖文件,以空格分开,这些依赖文件的修改日期比目标的创建日期晚。
$@ 目标的完整名称。
$^ 所有的依赖文件,以空格分开,不包含重复的依赖文件。
$% 如果目标是归档成员,则该变量表示目标的归档成员名称。
注意前方高能,Makefile的最终展现
Makefile详细图解
实际编译结果
Makefile编译结果
看着挺乱的吧,稍微改一改,双手奉上Makefile代码。
#最后形成的Makefile
INCL=-I${HOME}/incl
BIN=$(HOME)/bin
OBJ1=hellocpp.o
OBJ2=hello.o
.SUFFIXES: .cpp .c
.cpp.o:
g++ ${INCL} -c $<
.c.o:
gcc ${INCL} -c $<
all: hellocpp hello
#C++编译
hellocpp:${OBJ1}
@echo "============开始编译============"
g++ -o $@ $?
@rm -f ${OBJ1}
@mv $@ ${BIN}
@echo "============编译结束============"
@echo ""
#C编译
hello:${OBJ2}
@echo "============开始编译============"
gcc -o $@ $?
@rm -f ${OBJ2}
@mv $@ ${BIN}
@echo "============编译结束============"
@echo ""
命令前加@,表示当前命令不显示,最后编译结果是这样。
Makefile编译结果
以上是Makefile的全部内容,本文完结。
分享几个Makefile的示例DEMO,
编译SOCKET服务,使用mysql数据库。
编译socket服务
Makefile代码如下
#socket服务端编译(用到mysql数据库)
INCL=-I/usr/local/mysql/include -I$(HOME)/incl
LIB=-L/usr/local/mysql/lib -lmysqlclient -lmysqld -lmysqlservices -L$(HOME)/lib -lbanktest
BINDIR=$(HOME)/bin
.SUFFIXES: .cpp .c
.cpp.o:
g++ ${INCL} -c $<
.c.o:
gcc $(INCL) -c $<
all: clean server
server:server.o
@echo "============开始编译============"
gcc -o $@ $? $(LIB)
@mv $@ $(BINDIR)
@echo "============编译结束============"
clean:
@rm -f *.o
编译des md5 base64密码服务的Makefile
#编译des md5 base64密码服务
INCL=-I/usr/local/mysql/include -I$(HOME)/incl
LIB=-L/usr/local/mysql/lib -lmysqlclient -lmysqld -lmysqlservices
BINDIR=$(HOME)/bin
LIBDIR=$(HOME)/lib
.SUFFIXES: .cpp .c
.cpp.o:
g++ ${INCL} -c $<
.c.o:
gcc $(INCL) -c $<
all: clean des md5 base64
des:des.o main_des.o
gcc -o $@ $? $(LIB)
mv $@ $(BINDIR)
md5:md5.o main_md5.o
gcc -o $@ $? $(LIB)
mv $@ $(BINDIR)
base64test:base64.o main_base64.o
gcc -o $@ $? $(LIB)
mv $@ $(BINDIR)
rsa:rsa.o main_rsa.o
gcc -o $@ $? $(LIB)
mv $@ $(BINDIR)
libjiami.a:des.o md5.o base64.o
ar -r $@ $?
mv $@ $(LIBDIR)
libdestest:main_des.o
gcc -o $@ $? $(LIB) -L$(HOME)/lib -ljiami
mv $@ $(BINDIR)
libtest.so:des.c md5.c base64.c
gcc -o $@ -fPIC -shared $?
mv $@ $(LIBDIR)
libmd5test:main_md5.o
gcc -o $@ $? $(LIB) -L$(HOME)/lib -ltest
mv $@ $(BINDIR)
libbanktest.a:banktest.o banksql.o
ar -r $@ $?
mv $@ $(LIBDIR)
banktest:banktest.o banksql.o
gcc -o $@ $? $(LIB) -L$(HOME)/lib -ltest
mv $@ $(BINDIR)
clean:
rm -f *.o
最后,加几点写Makefile的注意事项
1. tab分隔,不能用空格。
2. 每个makefile最好加一个all
3. 注释用“#”符号
4. 文件指示,引用其他的makefile文件
make
原文地址:https://www.cnblogs.com/tp-16b/p/8955462.html
makefile带来直接好处就是——“自动化编译”。一旦写好,只需要一个make命令,整个工程完全自动编译,所以十分方便。而Makefile文件就是告诉make命令怎么样地去编译和链接程序。但是想要比较灵活的运用它,还是先要熟悉一些关于系统对程序编译和链接的知识。
一般来说,对C、C++程序、先把源文件编译成中间代码文件。Linux下是 .o 文件即 Object File,在Windows下也就是 .obj 文件,这个动作叫做编译(compile)。然后再把大量的.O文件合成执行文件,这个动作叫作链接(link)
编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是让我们告诉编译器头文件的所在位置(头文件中放声明,而定义放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(.O文件或是OBJ文件)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(.O文件或.OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件。在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在Linux下,是Archive File,也就是 .a 文件
总的来说就是,首先源文件-> .o文件,再由.o文件->可执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的.o文件中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)
来个例子感受一下,
hello: hello.o
hello.o: hello.c
gcc -c hello.c -o hello.o
这里make,便会自动编译了。这当中生成可执行文件hello依赖于hello.o,hello.o 依赖于 hello.c; 最后找到了hello.c便可以gcc生成hello.o这样往后‘带’,目标文件的hello便链接上.o文件去执行了。这里值得注意的是写gcc命令时需要添上 -c选项,用来保证得到的.o文件可重链接,不然基本会make报错(某些情况如直接gcc hello.c -o hello例外)。
直白点说,最后生成的可执行文件就是链接.o文件得到;而.o文件靠着“依赖关系”生成。
还有注意一点就是在Makefile中的命令(如gcc ..),必须要以[Tab]键开始,不然你很可能就会make出错哦~。
linux编译动态库和静态库的makefile示例
版权声明:本文为CSDN博主「有来有去-CV」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shaoxiaohu1/article/details/46943417
根据GenDll.cpp文件,分别生成动态库.so和静态库.a文件,需要依赖的外部库为opencv。
1. 静态库的生成
makefile命令的简介可参考:跟我一起写 Makefile。使用ar命令生成.a文件,可参考:Linux下动态库(.so)和静态库(.a)
# 1、准备工作,编译方式、目标文件名、依赖库路径的定义。 CC = g++ CFLAGS := -Wall -O3 -std=c++0x # opencv 头文件和lib路径 OPENCV_INC_ROOT = /usr/local/include/opencv OPENCV_LIB_ROOT = /usr/local/lib OBJS = GenDll.o #.o文件与.cpp文件同名 LIB = libgendll.a # 目标文件名 OPENCV_INC= -I $(OPENCV_INC_ROOT) INCLUDE_PATH = $(OPENCV_INC) LIB_PATH = -L $(OPENCV_LIB_ROOT) # 依赖的lib名称 OPENCV_LIB = -lopencv_objdetect -lopencv_core -lopencv_highgui -lopencv_imgproc all : $(LIB) # 2. 生成.o文件 %.o : %.cpp $(CC) $(CFLAGS) -c $< -o $@ $(INCLUDE_PATH) $(LIB_PATH) $(OPENCV_LIB) # 3. 生成静态库文件 $(LIB) : $(OBJS) rm -f $@ ar cr $@ $(OBJS) rm -f $(OBJS) tags : ctags -R * # 4. 删除中间过程生成的文件 clean: rm -f $(OBJS) $(TARGET) $(LIB)
2. 动态库的生成
第1、4步准备和收尾工作与静态库的保持一致,第2步和第3步所使用的命令稍有不同。
# 1、准备工作,编译方式、目标文件名、依赖库路径的定义。 CC = g++ CFLAGS := -Wall -O3 -std=c++0x # opencv 头文件和lib路径 OPENCV_INC_ROOT = /usr/local/include/opencv OPENCV_LIB_ROOT = /usr/local/lib OBJS = GenDll.o #.o文件与.cpp文件同名 LIB = libgendll.so # 目标文件名 OPENCV_INC= -I $(OPENCV_INC_ROOT) INCLUDE_PATH = $(OPENCV_INC) LIB_PATH = -L $(OPENCV_LIB_ROOT) # 依赖的lib名称 OPENCV_LIB = -lopencv_objdetect -lopencv_core -lopencv_highgui -lopencv_imgproc all : $(LIB) # 2. 生成.o文件 %.o : %.cpp $(CC) $(CFLAGS) -fpic -c $< -o $@ $(INCLUDE_PATH) $(LIB_PATH) $(OPENCV_LIB) # 3. 生成动态库文件 $(LIB) : $(OBJS) rm -f $@ g++ -shared -o $@ $(OBJS) rm -f $(OBJS) tags : ctags -R * # 4. 删除中间过程生成的文件 clean: rm -f $(OBJS) $(TARGET) $(LIB)
-fpic 和 -shared 命令可参考:Linux下动态库(.so)和静态库(.a)【注】这篇文章说可以使用ld命令生成.so文件,但我在测试时发会报错。
3. 动态库和静态库的调用
, 这两个的使用方法几乎没有区别。动态库的引用有显式和隐式两种,这里只说隐式调用。我使用main.cpp来测试生成的库文件, makefile如下:
CC = g++ CFLAGS := -Wall -O3 -std=c++0x OPENCV_INC_ROOT = /usr/local/include/opencv OPENCV_LIB_ROOT = /usr/local/lib MY_ROOT = ../ OPENCV_INC= -I $(OPENCV_INC_ROOT) MY_INC = -I $(MY_ROOT) EXT_INC = $(OPENCV_INC) $(MY_INC) OPENCV_LIB_PATH = -L $(OPENCV_LIB_ROOT) MY_LIB_PATH = -L $(MY_ROOT) EXT_LIB = $(OPENCV_LIB_PATH) $(MY_LIB_PATH) OPENCV_LIB_NAME = -lopencv_objdetect -lopencv_highgui -lopencv_imgproc -lopencv_core MY_LIB_NAME = -lgendll all:test test:main.cpp $(CC) $(CFLAGS) main.cpp $(EXT_INC) $(EXT_LIB) $(MY_LIB_NAME) $(OPENCV_LIB_NAME) -o test
4. 注意事项:
1、在测试过程中,经常会报错:找不到.so文件。一种简单的解决方法如下:
在linux终端输入如下命令:
export LD_LIBRARY_PATH=/home/shaoxiaohu/lib:LD_LIBRARY_PATH:
更多解决方法可参考:Linux下gcc编译生成动态链接库*.so文件并调用它的第4部分。
Makefile概述
基本格式
基本上每一个 Makefile 主体就是由若干个以下规则模块组成 : 表明输出的目标,输出目标的依赖对象和生成目标需要执行的命令。
target ..: prerequisites ...
recipe
....
target: 目标、标记。可以是文件,比如下文 基本例子 中的 edit 或者 main.o,也可以是标签,比如 clean。prerequisites: 先决条件,前提依赖,指明想要 target,需要先有哪些依赖。如 基本例子 中,要输出 edit, 需要先编译 main.o...等文件。recipe: 执行的命令。每行命令以tab开头(.RECIPEPREFIX 默认规定的)
举个例子, 我们写了 hello.c 打印可爱的 hello world,一般编写后,我们在命令行执行 gcc -o hello hello.c 生成 hello 可执行文件。这里, hello 就是我们的目标 target, 而 gcc -o hello hello.c 就是命令 recipe, 对应的依赖 prerequisites 就是 hello.c。
所以可以写第一个 Makefile:
hello: hello.c
gcc -o hello hello.c
然后直接在目录下输入make,就可以得到 hello。
Makefile 主要组成
- 显式规则
明确写出来的依赖关系,如上述的 prerequisites.... - 隐式规则
Make 自己推导出来的规则,比如目标为 main.o 就推出依赖条件中需要 main.c和对应的编译命令 - 变量定义
类似程序中宏定义, 文本替换。 - 文件指示 (有点像程序中预编译涉及到的.)
- include 其他 Makefile :
-include xx.md; - 选择执行(类似选择编译 #ifdef...);
- 定义多行命令(define ... endef)
- include 其他 Makefile :
- 注释
以 # 开头
Make 工作流程
- Make 读取当前目录下命名为
GNUmakefile/Makefile/makefile或者 -f 直接指定的所有规则文件。 - 读入被 include 的其他 Makefile,在对应位置展开
- 初始化变量
- 推导隐式规则;分析所有规则,创建依赖关系链,决定哪些需要【重新】生成,执行命令。
- 从第一个 target(排除以 . 开头的 target,比如 .PHONY)开始,这个就是默认目标,本次任务的终极目标(或者可以显式设定
.DEFAULT_GOAL)。 比如在下面的 基本例子 中,edit就是终极目标。 - 判断目标是否存在, 依赖的对象是否有更新
- 根据依赖关系一步一步追溯查找,建立依赖关系链,执行需要执行的命令,最终输出终极目标。
- 没有在依赖链上的目标是不会被直接执行到的,比如 clean。
- 从第一个 target(排除以 . 开头的 target,比如 .PHONY)开始,这个就是默认目标,本次任务的终极目标(或者可以显式设定
基本例子
借用手册举的例子,有一个工程,由几个.c 和 .h 文件组成,最终输出可执行文件 edit,简单(简陋..?)地用以下 makefile 描述他们的依赖关系。
objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o # make读取到的第一个target, 默认设置为 终极目标 edit: $(objects) cc -o edit $(objects) # 隐含规则, make 根据 xx.o 推导出依赖 xx.c, 以及相应的编译命令 # 如 -> main.o: defs.h # 等同 : # -> main.o: main.c defs.h # -> cc -o main.o main.c defs.h main.o: defs.h kbd.o: defs.h command.h command.o: defs.h command.h display.o: defs.h buffer.h insert.o: defs.h buffer.h search.o: defs.h buffer.h files.o: defs.h buffer.h command.h utils.o: defs.h # .PHONY 定义为目标 clean # 和 edit 没有依赖关系 直接执行 make 是不会运行到的 # 通过 make clean 执行 .PHONY: clean clean: -rm edit $(objects)**
编写规则
Make 会读取 Makefile 中 第一个规则的第一个目标, 设置为要完成的最终目标。其他目标都是被这个目标的依赖关系连带进来的。比如基本例子中最终目标是 edit,而 eidt 依赖 main.o...等文件链接而来,所以 make 就会自动去执行 main.o..等文件的生成规则,最后再合成 edit。
规则包含 : 依赖关系 和 生成目标的方法
把上面的 Makefile 修改一下:
# Makefile learn by lcd SRCS = main.c SRCS += command.c SRCS += display.c SRCS += files.c SRCS += insert.c SRCS += kbd.c SRCS += search.c SRCS += utils.c # 语法替换 OBJS = $(SRCS:.c=.o) DEPS = $(SRCS:.c=.d) # 第一个目标 all,终极目标 all : edit edit : $(OBJS) $(CC) -o edit $(OBJS) -include $(DEPS) # 包含触发下面的 DEPS 依赖 $(DEPS) : %.d : %.c @set -e; rm -f $@;\ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;\ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;\ rm -f $@.$$$$ .PHONY : clean clean : @-rm edit $(OBJS) $(DEPS)
说明下规则
取其中一段
edit : $(OBJS)
$(CC) -o edit $(OBJS)
# 在这里实际展开应该是
edit : main.o command.o ...
cc -o edit main.o command.o ...
- 第一行说明文件的依赖关系,edit 是由 main.o command.o... 这几个文件链接而成的,依赖于他们。如果其中某个或多个 xx.o... 文件日期比 edit 新或者 edit 不存在,那么依赖关系就发生了。
- 发生依赖关系,Make 就会去执行下面的命令(tab缩进),其说明 edit 是如何通过依赖对象生成的。Make 会以 shell(/bin/sh)来执行命令。
- 上面这段规则,目标targets 是 edit, Makefile 中,targets 是文件名也可以是标号(比如clean),多个用空格分开,可以使用通配符(shell)。
Make 搜寻文件
实际中,比较大的工程文件都会分类放在不同目录下,当 Make 需要寻找文件依赖关系的时候,需要告知去寻找的路径,否则 make 只会查找当前目录。 两种方法:
- VPATH (变量)
VPATH = src:../inc
如上,指定了 ./src 和 ../inc 两个目录,冒号分隔,当前目录 搜索不到依赖文件的情况下,Make 就会依顺序进行搜索。
- vpath (关键字)
注意:这不是一个变量,按照使用方式可以多次调用设定文件的搜索模式。
vpath 使用的三种方法
1、vpath <pattern> <directories>
为符合模式<pattern>的文件指定搜索目录<directories>。
2、vpath <pattern>
清除符合模式<pattern>的文件的搜索目录。
3、vpath
清除所有已被设置好了的文件搜索目录。
vapth 使用方法中, <pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符。例如,“%.h”表示所有以“.h”结尾的文件。<pattern>指定了要搜索的文件集,而 <directories>则指定了<pattern>的文件集的搜索的目录。
vpath %.c dir1 # 在 dir1 寻找 .c 文件 vpath % dir2 # 在 dir2 寻找 任何需要的文件 vpaht %.c dir3 # 同 1 # 当前目录找不到的情况下i, 按照 dir1.2.3的顺序查找 .c 文件
伪目标
上面例子中,clean 就是一个伪目标。伪目标是一个标签,执行一些动作,比如清除文件,安装程序等。因为没有依赖关系,所以 make 无法直接决定是否需要执行。我们显示地用 .PHONY来告诉 make 这是一个伪目标, 避免与实际目标命名冲突。
同运行程序的时候我们给个参数让程序执行特定动作一样,运行 make 时指定伪目标标签,指定执行对应的命令。就如上述例子,执行 make clean 时进行清理工作。
静态模式
对应多个目标对象,构建每个对象对应名称的依赖关系的规则。
举个例子怎么用
OBJS = aa.o bb.o cc.o $(OBJS) : %.o : %.c cc -c $< -o $@ # 等同: aa.o : aa.c cc -c aa.c -o aa.o bb.o : bb.c cc -c bb.c -o bb.o cc.o : cc.c cc -c cc.c -o cc.o
上述例子,Make 从 OBJS 集合中获取符合 目标模式 %.o 的文件作为目标,依赖模式 %.c 取前面获取的“%.o”的“%” 部分作为自己的前缀。在文件很多的情况下,可以大大提高了书写效率。
自动生成依赖关系
如果在 main.c 中包含了 defs.h 文件,那么依赖关系上我们需要写上 defs.h,这样,当 defs.h 文件修改了(比如新定义了一个宏..),Make 才会重新执行依赖关系。但是对于一个文件包含什么头文件,对应修改 Makefile,这样是很难维护的。
C/C++ 编译器 -MM 功能可以自动找寻文件的包含 ,生成依赖关系。
执行:
$ gcc -MM mian.c
输出:
main.o : main.c defs.h
因此,我们借助编译器帮我们自动生成依赖关系,并包含到 Makefile 中
-include $(DEPS) $(DEPS) : %.d : %.c @set -e; rm -f $@;\ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;\ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;\ rm -f $@.$$$$
上述的 -include 把每个源文件对应的依赖 [.d] 文件(gcc -MM生成的依赖关系)包含进来,把 [.d] 文件的更新也纳入 Makefile 中,修改了某个文件的依赖关系,对应命令执行生成新的依赖文件。
上面的命令,每个[.d] 文件依赖对应的[.c]文件,具体说明下执行命令的作用:
- $@ $* 和 $< 是自动变量
rm -f $@删除旧的目标文件- 借助编译器(-MM)为每个源文件生成依赖关系并保存到对应的 name.xxxx (在Makefile中 $ 有特殊含义,如果要表示它的字面意思需要写两个 $,所以 Makefile 中的四个 $ 传给Shell变成两个 $,而两个 $ 在Shell中表示当前进程的id,一般用它给临时文件起名,以保证文件名唯一。)
- sed 替换输出文本,达到的目的类似如下
# 编译器输出格式 main.o : main.c defs.h # 转换为如下格式 main.o main.d : main.c defs.h
并保存到[.d]文件。
- 删除临时文件
最后展开就如同开头例子一样,列出每个[.o]文件的依赖, 相比前面似乎更加复杂了,但是想想,在很多源文件的情况下,就会变得很简洁。
编写执行的命令
target .. : prerequisites ...
recipe
....
上述 recipe 命令部分由若干条 shell 命令行组成,一般用于生成、更新对象。 默认使用 /bin/sh执行命令。
默认每行命令必须以 Tab 开头。规则下对应的所有以 Tab 开头的指令,会被传递到对应的 shell 执行。
Makefile 执行指令必须在 recipe 这个位置。
在命令中使用变量注意
前面提到, $ 在 make 和 shell 中要注意的地方, 再举个例子
LIST = one two three all: for i in $(LIST); do \ echo $$i; \ done
实际传递给 shell 的模式
for i in one two three; do \ echo $i; \ done
结果输出 :
one
two
three
避免错误使用给自己带来的错误。
命令回响
在 Makefile 中执行如下命令,
echo 命令执行
终端会输出如下 :
echo 命令执行
命令执行
第一行是执行的命令完整打印(回响),第二行才是我们需要的输出的,关闭命令回响的方法是在该行命令前添加 @
@echo 命令执行
如果 Make 执行时,带参数“-n”或“--just-print”,那么其只是显示命令,不会执行命令,这个功能有利于我们调试我们的 Makefile,看看我们书写的命令执行起来是什么样子的或是什么顺序的。 而 Make 带参数“-s”或“--slient”则是全面禁止命令的显示。
命令的依赖
shell 按顺序一条条执行规则指定的命令。但是如果需要让上一条命令的结果应用到下一条,需要用分号分隔命令并保证命令处于同一行。
假设在目录 /home/lcd/mf/ 下执行 Makefile
exec1 : @cd /home/lcd/kk @pwd # show : /home/lcd/mf exec2 : @cd /home/lcd/kk; pwd # show : /home/lcd/kk
exec1 显示的依然是 Makefile 执行的所在目录,因为上一条命令的结果没有应用到下一条。
忽略出错命令
一般情况,Make 会一条一条执行命令,当某条命令执行后出错, Make 会终止当前规则,这可能导致整个任务终止。
有时候执行一些命令无需考虑出错,比如某文件存在删除,不存在就不管等。 这种情况下不希望出错终止,可以在任务前添加一个减号 -
clean :
-rm -f *.o
一个全局方法是, Make 运行加上“-i”或是“--ignore-errors”参数,那么,
Makefile 中所有命令都会忽略错误。
如果一个规则是以“.IGNORE”作为目标的,那么这个规则中的所有命令将会忽略错误。
Make 的参数的是“-k”或是“--keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。
Makefile 嵌套
对于一个比较大的工程,不同模块分类在不同目录,分别用一个 Makefile 进行管理,模块化编译,方便工程维护和保证 Makefile 的简洁。
- 例如,子目录 subdir 下有一个 Makefile 描述该目录模块的编译规则, 那么总控 Makefile 中调用子目录 Makefile 可以这么写:
subsystem : cd subdir && $(MAKE) # 等价 subsystem : $(MAKE) -C subdir
使用
$(MAKE)宏定义,在某些情况下调用嵌套我们可以直接修改添加参数。另外,当运行 Make 时候添加诸如 ‘-t’ (‘--touch’), ‘-n’ (‘--just-print’), or ‘-q’ (‘--question’) z这些特殊选线时,使用$(MAKE)可以保证语句相当于在前面添加了+号 的作用(特殊命令,继续执行)。很正常,希望测试的时候命令不是真的执行,但是包含其他 Makefile 这种命令是例外,必须执行,不然 Makefile 就不完整了, 我是这么理解的。
- 上层 Makefile 中定义的变量是可以在被调用的下一层 Makefile 中使用的, 前提是该变量在上层中被显式暴露
export,同理,可以采用unexport取消。
export OBJS # 传递 变量 OBJS
export # 不指定,全部传递
如此,在下面的 makefile 就可以直接使用了。但是如果下层目录已经定义了该变量,那么下层默认使用的是它自己定义的变量值,除非上层 makefile 在调用下层 makefile 时给参数 -e,则会强行覆盖。
-
两个变量,一个是 SHELL,一个是 MAKEFLAGS,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中。
-
Make 命令中的有几个参数并不往下传递,它们是“-C”,“-f”,“-h”“-o”和“-W”
-
嵌套执行中, “-w”或是“--print-directory”会在 Make 的过程中让你看到目前的工作目录。
比如,如果我们的下级 Make 目录是/home/lcd/mf/subdir,如果我们使用“make -w”来执行,那么当进入该目录时,我们会看到:make: Entering directory '/home/lcd/mf/subdir'
而在完成下层 make 后离开目录时,我们会看到:make: Leaving directory `/home/lcd/mf/subdir'
当你使用“-C”参数来指定 Make 下层 Makefile 时,“-w”会被自动打开的。如果参数中有“-s”(“--slient”)或是“--no-print-directory”,那么,“-w”总是失效的。
命令组宏定义
和程序中的宏定义,展开一样。因为直接展开,注意缩进问题。
define xxx_name xxx xxx endef targe : xx $(xxx_name) #等同 targe : xx xxx xxx
使用空命令
target : ;
上述规则, 执行了一条空指令。这样写的一些理由是:
- 避免 Make 自己推测命令(隐性规则)
- Make 不会报错他不知道该对象如何生成,并假设已经是最新。
Makefile 中的变量
Makefile 中的变量,就如程序中的宏定义,代表一个字串,在使用的地方展开,通过 $(variable)表示变量的内容,和 shell 类似。
变量赋值
foo = $(bar) bar = $(ugh) ugh = huh? all : echo $(foo) # 显示 : huh?
赋值 ?=, +=, = 和 := 的差别
- ?= 如果没有被赋值过就赋予等号后面的值
- += 添加等号后面的值
- = 最基本的赋值(最后才展开)
make会将整个makefile展开后,再决定变量的值。也就是说,变量的值展开是在最后, 使我们可以在最后才指定变量的值。
x = XXX
y = $(x)
x = YYY
在上例中,y的值将会是 YYY ,而不是 XXX。
- := 是覆盖之前的值(类似C中的 = )
变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。
x := XXX
y := $(x)
x := YYY
在上例中,y的值将会是 XXX ,而不是 YYY了。
变量值替换
foo := a.o b.o c.o bar := $(foo:.o=.c) # 等同 foo := a.o b.o c.o bar := $(foo:%.o=%.c) # bar : a.c b.c c.c
定义一个空格
nullstring :=
space := $(nullstring) # end of the line
操作符号对右边一开始的空格不做处理,所以很难描述一个空格,上面的方法实现了,sapce 保存一个空格。
override 指示符
如果在运行 Make 的时候在命令参数设置了变量,则 Makefile 对变量的设置默认被忽略,如果不想被忽略,可以使用override。
override <variable> = <value> override <variable> := <value> override <variable> += <more text>
模式变量
对应变量只应用到符合模式的对象上
%.o : CFLAGS = -O
# <pattern ...> : override <variable-assignment>
自动变量
为了方便扩展, 经常会看到类似的一些奇怪符号。
- $@
$@ 指代当前目标,Make 命令当前构建的那个目标。比如,make foo 的 $@ 就指代 foo。
a.txt b.txt: touch $@ #等同于下面的写法。 a.txt: touch a.txt b.txt: touch b.txt
- $<
$< 指代第一个依赖条件。
如果依赖对象是一个序列,依次代表每一个依赖条件
a.txt: b.txt c.txt cp $< $@ #等同于下面的写法。 cp b.txt a.txt cp c.txt a.txt
-
$^
$^ 指代所有前置条件,之间以空格分隔。
比如,规则为t: p1 p2,那么 $^ 就指代 p1 p2 。 -
$?
$? 指代比目标更新的所有前置条件,之间以空格分隔。
比如,规则为t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。 -
$*
$* 指代匹配符 % 匹配的部分,
比如 %.txt 匹配 f1.txt 中的 f1 ,$* 就表示 f1。 -
$(@D)和$(@F)$(@D)和$(@F)分别指向 $@ 的目录名和文件名。
比如,$@ 是 src/input.c,那么$(@D)的值为 src ,$(@F)的值为 input.c。 -
$(<D)和$(<F)$(<D)和$(<F)分别指向 $< 的目录名和文件名。
条件判断
简述
类比程序中的条件编译, Make 可以根据运行时不同情况选择执行不同分支。
libs_for_gcc = -lgnu normal_libs = foo : $(OBJS) ifeq ($(CC), gcc) # 不缩进 $(CC) -o foo $(OBJ) $(libs_for_gcc) # 传递命令,缩进 else $(CC) -o foo $(OBJ) $(normal_libs) endif
简单地说,上面表达的是,如果使用的编译器是 gcc,则编译时添加参数libs_for_gcc,否则给另一个参数normal_libs。
其实和 C 中的条件编译差不多
注意
条件语句部分不需要缩进, 否则会被认为是传递给 shell 的命令
Make 条件判断语法
看起来和 shell 中的条件判断差不多,
分支组成
# if-endif conditional-directive text-if-true endif # if-else-endif conditional-directive text-if-true else text-if-false endif # if-elsif0-elsif2..-elsifn-else-endif conditional-directive-one text-if-one-is-true else conditional-directive-two text-if-two-is-true else text-if-one-and-two-are-false endif # 多分支例子 ifeq $(STRING), 'AA' echo AA else ifeq $(STRING), 'BB' echo BB else echo XX endif
判断关键词
- 判断相等
完全展开变量进行比较
ifeq (ARG1, ARG2) ifeq 'ARG1' 'ARG2' ifeq "ARG1" "ARG2" ifeq "ARG1" 'ARG2' ifeq 'ARG1' "ARG2" # 对应 #ifneq
手册举的例子,用于判断变量是否为空
ifeq ($(strip $(foo)),)
text-if-empty
endif
- 判断是否定义
ifdef variable_name
ifndef variable_name
注意例子, 只做第一次展开!!!!,体会下一下例子差别
bar = foo = $(bar) ifdef foo # 展开 foo -> $(bar) # 所以认为定义了, 即使 foo 最终是空 echo foo def endif ifdef bar # 展开 bar, 空,认为未定义 echo bar def endif
对于嵌套 Makefile, 不允许一个完整的 if-endif 语句跨越两个 Makefile
例子,判断执行 flag
函数 findstring 用于判断 A 字符串是否在 B 字符串, 没有返回空,有返回 A
下面例子, 根据是否带有“-t” 参数执行不同命令。
archive.a: ... ifneq (,$(findstring t,$(MAKEFLAGS))) # 如果执行make -t, 则执行这里 # -t touch, 表示实际不执行命令,但是这里需要这个参数后命令 # 依旧执行,所以前缀 ``+`` +touch archive.a +ranlib -t archive.a else ranlib archive.a endif
函数调用
调用语法
函数可以出现在任何变量可以出现的位置,对变量进行文本处理。
$(function arguments) # 风格 1
${function arguments} # 风格 2
# 选一种 别混用 统一好看避免不必要麻烦
如上, 对应下面例子中, function 对应“subst”, arguments 对应“$(space),$(comma),$(foo))”, “subst”这个函数提供替换字符功能
comma:= , empty:= space:= $(empty) $(empty) foo:= a b c bar:= $(subst $(space),$(comma),$(foo)) # foo 中的空格替换为逗号 # bar is now ‘a,b,c’.
function 后面对应传递的参数,第一个参数与函数名通过空格或者 tab 划分,如果一个函数参数不止一个,不同参数通过逗号分隔。函数调用返回,通过 $ 获取,和变量使用一致。
字符串处理函数
文本替换函数
- 简单替换
将“text”中的“from”部分替换为“to”
$(subst from ,to ,text) # 例子 # 返回 : ‘fEEt on the strEEt’. $(subst ee, EE, feet on the street)
- 模式替换
读取函数名后面的模式, 匹配“text”中符合的部分替换为第二个参数指定的内容。
$(patsubst pattern ,replacement ,text) # 例子 1 # 模式替换,同时, 多个空格会被折叠为一个 # 返回 :x.c.o bar.o $(patsubst %.c, %.o, x.c.c bar.c) # 例子 2 objects = foo.o bar.o baz.o $(patsubst %.o, %.c, $(objects)) # equil to $(objects:.o=.c)
去空格函数
去除字符串开头和结尾的空格,同时对中间的多个空格替换为一个。
$(strip string)
# 例子
# 返回: a b c
$(strip a b c )
在判断变量是否为空的情况下使用,可以避免多次赋值带来的空格影响,提高鲁棒性
字符查找函数
判断字符中是否包含指定字符串, 有返回查找的字符串,否则返回空。
$(findstring find ,in) # 例子 # 返回 : a $(findstring a,a b c) # 返回 : "" $(findstring a,b c)
字符串模式过滤
返回符合或者不符合的字符串, 输入字符单词空格区分
- 返回符合的字符串
$(filter pattern ...,text) # 例子 # 返回 foo.c bar.c baz.s sources := foo.c bar.c baz.s ugh.h $(filter %.c %.s, $(sources))
- 返回不符合的字符串
$(filter-out pattern ...,text) # 例子 # 返回 foo.o bar.o objects = main1.o foo.o main2.o bar.o mains = main1.o main2.o $(filter-out $(mains), $(objects))
排序、去重函数
按字母顺序对序列(空格划分)进行排序,同时去除重复的词组, 返回按单个空格进行划分。
$(sort list)
# 例子
# 返回 : bar foo lose
$(sort foo bar lose lose)
字符串切片函数
数组数组
$(word n ,text) # 返回 : bar $(word 2, foo bar baz) $(wordlist s ,e ,text) # 返回 : bar baz $(wordlist 2, 3, foo bar baz) $(words text) # 字符串成员个数 空格划分 # 返回 : 4 $(words aa bb cc dd) $(firstword names ...) # 返回 : foo $(firstword foo bar) $(lastword names ...) # 返回 : bar $(lastword foo bar)
举个实际例子
C 编译器编译参数 -I 后带路径,下面例子通过 VPATH 生成 CFLAGS 变量供编译器使用。
VPATH = src:../headers # 空格 替换原来的分隔符号 # 返回 : src ../headers $(subst :, ,$(VPATH)) # 加上-I 前缀 # 返回 : -Isrc -I../headers override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
文件名处理函数
$(dir names ...) # 返回除去最后文件名的路径部分, 没有路径直接返回“./” # 返回 : lcd/src/ ./ $(dir lcd/src/foo.c hacks) $(notdir names ...) # 返回不包含目录的文件名 # 返回 : foo.c hacks $(notdir src/foo.c hacks lcd/) $(suffix names ...) # 返回文件后缀(逆序第一个 . 后面字符串) # 返回 : .c .c .o $(suffix src/foo.c src-1.0/bar.c hacks lcd.c.o) $(basename names ...) # 去除后缀, 文件目录内的后缀(.)不包括 # 返回 : src/foo src-1.0/bar hacks $(basename src/foo.c src-1.0/bar hacks) $(addsuffix suffix, names ...) # 加后缀 # 返回 : foo.c bar.c $(addsuffix .c, foo bar) $(addprefix prefix, names ...) # 加前缀 # 返回 : src/foo src/bar $(addprefix src/, foo bar) $(join list1, list2) # 对应连接参数 # 返回 : a.c b.o $(join a b, .c .o) $(wildcard pattern ) # 获取工作目录下所有符合模式的文件 # 返回所有 .C 文件 $(wildcard *.c) $(realpath names ...) # 返回绝对路径, 不包含 . 或者 .. # 如果文件不存在,返回空 $(abspath names ...) # 返回绝对路径, 不包含 . 或者 ..
条件函数
- if
如果 condition 为真, 返回 then-part 代表的值, 否则放回 else-part 的值(没有的话默认返回空)。
$(if condition,then-part [,else-part]) # 例子 # 返回 : false con = $(if $(conn), "true", "false")
- or
依次展开每个参数,遇到非空的就停止,并返回该值,否则最后返回空。
$(or condition1 [,condition2 [,condition3 ...]]) # 例子 # 返回 : CONN2 conn1 = conn2 = CONN2 conn3 = CONN3 $(or $(conn1), $(conn2), $(conn3))
- and
展开所有参数,如果有一个为空,返回空,如果全部都不为空,返回最后一个参数。
$(and condition1 [,condition2 [,condition3 ...]]) # 例子 1 # 返回 : 空, 因为有一个空 conn1 = conn2 = CONN2 conn3 = CONN3 $(and $(conn1), $(conn2), $(conn3)) # 例子 2 # 返回 : CONN3 conn1 = CONN3 conn2 = CONN2 conn3 = CONN3 $(and $(conn1), $(conn2), $(conn3))
循环函数 foreach
这个函数执行过程, 按顺序依次取出 list 中的单词逐个取出放入到临时变量 var 中, 返回 text, 每次返回的 text 以空格分开,遍历所有单词后返回完整的组合字符串。
$(foreach var, list, text) # 例子 1 # 返回 a.c b.c c.c d.c e.c f.c list = a b c d e f $(foreach var, list, $(var).c) #例子 2 dirs := a b c d files := $(foreach dir, $(dirs), $(wildcard $(dir)/*)) # 等同于 files := $(wildcard a/* b/* c/* d/*)
读写文件函数
file 支持读写,通过 op 确定操作, 后跟操作文件和写入文本(读取的时候不能包含),写操作,如果文件不存在,会自动创建。
- “>” 覆盖写
- “>>” 追加写
- “< ” 读取
$(file op filename[,text])
没试过...
自定义函数
当make执行这个函数时,variable参数中的变量,如$(1),$(2),$(3)等,会被参
数 parm1, parm2,parm3 依次取代。而 variable 的返回值就是call函数的返
回值。例如:
$(call variable,param1,param2,...) # 例子 reverse1 = $(1) $(2) reverse2 = $(2) $(1) foo1 = $(call reverse1, a, b) foo2 = $(call reverse2, a, b) # foo1 == a b # foo2 == b a
可以通过 call 函数定义复杂的函数组合
value 函数
value 函数返回变量未经展开的值, 如例子
a = aabb b = $(a) echo $(b) # aabb echo '$(value b)' # $(a)
shell 函数
Makefile 中除了命令区域,是不能直接执行 shell 命令,但是可以通过 shell 函数执行,调用该函数,会生成一个新的程序,所以需要注意效率问题。
例子, 在 Makefile 中获取最后一个 git 提交的 SHA 赋值给变量。
FW_VER = $(shell git log -1 --pretty="%h")
Make 控制函数
用于在运行过程中提供信息, 打印 log
@echo $(info msg) @echo $(warning msg) @echo $(error msg) # error 中断执行
origin 函数
不操作变量, 返回变量定义的地方
eval 函数
flavor 函数
guile 函数
参考
GNU Make Manual
中文版-跟我一起写makefile
原文作者:orientlu 原文地址:https://www.jianshu.com/p/5982ccb87af0
Makefile由浅入深--教程:https://zhuanlan.zhihu.com/p/47390641

浙公网安备 33010602011771号