本文概述
- 前言
- Makefile构建任务是什么?
- Makefile规则
- Makefile变量使用
- Makefile路径搜索
- Makefile条件判断
- Makefile函数
- Makefile命令参数
- 多个Makefile:多模块项目构建
- 多项目构建模板:Makefile完整配置
- 总结
前言
我在上一篇文章中已经大篇幅详细介绍了Makefile文件的编写,但是写完感觉有些东西不是很清晰,所以本文打算重新介绍Makefile的编写。本文相对来说会比较有条理一些,而且估计文章会很长,看完需要一些耐心。
本文是尽可能完整介绍Makefile,当然不是完美的,官方的参考文档也很长,就没必要花时间去阅读了。因为make+Makefile只是基础,实际项目开发可以这样使用,但是还有更方便的——cmake+CMakeLists.txt,不过ake也是对make的高层封装,它还是通过生成Makefile来构建项目的。
因此花一些时间来学一下Makefile也挺有必要的,可以用于日常项目构建,不用那么也多多少少会遇到,而且Linux的相关开发,make+Makefile的项目还是很多的。
Makefile构建任务是什么?
(注意:以下提到gcc的地方,一般对g++同样适用)
至于为什么要使用Makefile就不再说了,但是学习Makefile,至少我们要知道用它来干什么。Makefile很复杂,可我们不是为了学复杂的东西而学的,如果不知道如何使用Makefile,那你学它来干什么呢?
Make是一个构建工具,而Makefile是一个构建脚本,所有Makefile的主要任务就是构建项目。但这还不够详细,更为具体地说,Makefile需要处理一系列的文件,并最终处理成目标文件。编译和链接只是处理文件的一种方式(翻译),C/C++项目处理的文件类型包括:
- 资源文件,图片、项目配置文件等。
- 源文件,实现文件,如*.c或*.cpp、*.cxx文件。
- 头文件,*.h文件。
- 中间文件,一般是*.o或*.obj文件,项目构建很少遇到其它中间文件,例如.s汇编中间文件也很少。
- 静态库,linux的*.a文件,windows的*.lib文件。
- 动态库,linux的.so文件,windows的.dll文件。
- 可执行文件,linux的ELF文件(无严格后缀),windows的PE文件(一般是.exe作为后缀)。
所以,Makefile的一个重要任务就是处理文件,或者说它主要就是处理文件,关键是我们自己要区分要处理的是什么文件,以及如何处理。
而对于文件处理的操作一般至少包括:
- 创建文件,一般可以使用shell命令完成。
- 删除文件,使用shell命令完成。
- 查找文件,使用shell或make内置的功能完成。
- 修改文件名,使用shell命令完成。
- 编译文件,使用gcc -c完成。
- 链接文件,使用gcc完成。
对于我们的C/C++项目,重点的几个处理(或最终目标)包括:
- 创建静态库:编译静态库中间文件使用gcc -c -I<HeaderDIR>,最终链接创建静态库使用ar rc lib<name>.a *.o。
- 创建动态库:编译动态库中间文件使用gcc -fPIC -I<HeaderDIR> -c,最终链接创建静态库使用gcc -shared -o lib<name>.so *.o。
- 创建可执行文件:直接使用gcc完成。
- 使用库文件:使用其它库进行编译中间文件使用 gcc -c I<HeaderDIR>,链接库需要同时指定库名称和库路径:gcc -L<libDIR> -l<libName>。
- 多模块编译:一个项目包含多个模块,每个模块使用自己的Makefile进行构建,项目根目录的Makefile作为全局控制的Makefile。
Makefile规则
规则的基本结构
Makefile的规则语法如下:
target ... : prerequisites ...
command
...
...
其中:
- Target为目标,一般为中间文件(.o、.obj文件)、可执行文件、标签,也可以是任意其它类型的文件,只是一般比较少。
- Prerequisites为依赖条件或依赖文件,它同样是一个target,依赖条件可以是一个文件名、目标,可以有多个或为空。第一个依赖为必要条件,依赖优先级从左到右降低,首先依赖的首先处理。编译中间文件的依赖一般为:依赖头文件和实现文件。
- Command为命令,命令左边必须是一个Tab,它是任意的shell命令,可以有零条或多条。Make默认使用bin/sh执行,由变量SHELL指定。
规则的代码形式如下:
targets: prerequisites
commands
还可以写成以下的形式:
targets: prerequisites; command
commands
make的注释使用#开始,一般都是单行注释,在代码中使用#需要转义,例如\#。
下面是一个简单项目的Makefile:
all: app
app: main.o
g++ -o app main.o
main.o: main.cpp
g++ -c -o main.o main.cpp
clean:
rm -rf app *.o
make的执行逻辑如下:
- make先获取当前目录的makefile文件。
- 查找第一个规则目标,例如上面的代码中all目标,将其作为最终目标。不管是什么规则,只要是第一个规则中的目标都会作为最终目标。
- Make默认支持增量构建,再次修改文件只会编译最新修改的文件,其它的不会编译。
要注意的是,目标(target)和依赖或条件(prerequisite)都可以当作一个文件名。规则的执行逻辑如下:
- 当依赖文件存在的时候,设A为目标文件更新时间戳,B为依赖文件更新时间戳(时间戳更大则更新)。
-
- A=B,不重建,不执行命令。
- A<B,重建,执行命令,重建后A=B。
- A>B,A一般不允许更改,不存在A比B更新的情况,除非你手动更改了A。
- 当依赖文件不存在的时候,重建,并寻找依赖文件对应的规则进行处理(获取依赖文件),如果依赖为空,则直接执行命令。
- 当目标文件不存在的时候,重建,并执行命令。
- 当命令为空的时候,表示目标文件依赖于依赖文件。
Makefile显式规则
显式规则就是那些自定义编写的构建规则。
命令编写
命令回显
命令以@开始不会显示这个将要被执行的命令,仅显示命令执行结果。不使用@会将该命令和执行结果都显示出来。
另外,在命令之前使用-可以忽略错误,例如我们可以在clean目标中添加:
clean:
-rm -rf app *.o
因为有时删除文件,如果没有文件可删除会显示错误信息,使用-表示无论如何,就当它执行成功了。
命令执行
Makefile中的命令,每一行命令将是在一个独立的子shell进程中执行,写在同一行中的多个命令属于一个完整的shell命令行,在独立行的命令是一个独立的shell命令行,多个独立行之间不互相影响。
Make支持并发执行命令,使用make -j <n>指定,其中n默认为1,它表示并发执行的数量。
定义命令集合
定义命令集合的语法如下:
define name
commands ...
endef
这类似于C中的宏定义,可以将命令集合的名称当做变量一样使用,下面是一个实际例子:
g++ -c -o main.o main.cpp
endef
all: app
app: main.o cal.o
g++ -o app main.o
main.o: main.cpp
$(compileMain)
cal.o: cal.cpp
g++ -c -o cal.o cal.cpp
clean:
rm -rf app *.o
目标类型
Makefile支持多种类型的目标,包括:
- 强制目标:在依赖中添加一个FORCE,表示总会被执行。
- 空目标文件:它是伪目标的一个变种,空文件,不关心其内容。
- 多规则目标:一个文件作为多个规则的目标,但只有最后一个目标的命令会作为重建命令。
另外还有一种特殊目标,如下列表:
- .PHONY: 伪目标,例如clean、all都是伪目标。
- .SUFFIXES: 指出了一系列在后缀规则中需要检查的后缀名。
- .DEFAULT: 用在重建那些没有具体规则的目标。
- .PRECIOUS: 目标所在的依赖文件在 make 的过程中会被特殊处理,文件不会被删除。
- .INTERMEDIATE: 目标的依赖文件在 make 执行时被作为中间文件对待。
- .SECONDARY: 目标的依赖文件被作为中过程的文件对待。
- .IGNORE:目标的依赖文件忽略创建这个文件所执行命令的错误。
- .DELETE_ON_ERROR: 规则的命令执行错误,将删除已经被修改的目标文件。
- .LOW_RESOLUTION_TIME: 目标的依赖文件被 make 认为是低分辨率时间戳文件。
- .SILENT: 不打印出创建此目标依赖文件所执行的命令。
- .EXPORT_ALL_VARIABLES: 作为一个简单的没有依赖的目标。
- .NOTPARALLEL: 有的命令按照串行的方式执行。
这么多目标应该很少用到吧?我也不知道,但是要注意的是伪目标,用法如下:
.PHONY: clean
clean:
rm -rf app *.o
你不用.PHONY声明也是可以的,只是可能你不小心写了一个clean文件,那么可能会产生一些问题,如果使用.PHONY声明,则不会将其与文件进行对应起来了。
Makefile隐式规则
上面说的显示规则需要我们自己手动编写,如果你写很多这些构建规则,就会发现其形式都差不多。Make默认就支持自动推导并使用默认的构建规则,例如n.o依赖于n.cpp和n.h。隐式规则是make预先设置的规则,隐含规则定义了一组标准的目标文件、依赖文件和命令,一般使用后缀名进行匹配,每条隐含规则都有对应的优先级,优先级高的优先使用。Make对支持的每种语言都定义了对应的隐含规则。
隐含规则一般是make由中间文件自动推到依赖文件和命令,其中最终目标不能省略,包括目标文件、依赖文件和命令。
下面的项目中有三个文件:main.cpp、cal.cpp和cal.h,使用隐含规则的例子如下:
all: app
app: cal.o main.o
g++ -o app $^
# 可使用以下无命令的规则定义文件之间的依赖
# 也可以省略这些依赖的声明
cal.o: cal.cpp cal.h
main.o: main.cpp
.PHONY: clean
clean:
rm -rf app *.o
虽然这些依赖声明可以省略,但是一般建议明确声明依赖,因为一个项目的某些文件编译可能有先后顺序,例如多模块的项目构建。
Make隐式规则使用的相关变量称为隐式变量,我们可以更改这些隐式变量的值,隐式规则仍然会使用这些变量。常见的隐式变量如下:
- CC:C编译器,默认cc。
- CXX:C++编译器,默认g++。
- CFLAGS:C编译选项。
- CXXFLAGS:C++编译选项。
- CPPFLAGS:C预处理选项。
- LDFLAGS:编译器链接选项。
对于依赖声明,使用隐式规则可省略声明依赖,但声明依赖可以避免文件特定依赖不匹配的错误,建议显式声明文件或项目模块的依赖。
伪目标:不会创建目标文件,一般用于执行指定的命令。
显示伪目标声明:.PHONY:(Name clean),否则伪目标有可能被当成文件
多目标:多个目标文件有相同的依赖。
通配符
- *:匹配0个或任意字符
- ?:匹配任意一个字符
- %:匹配任意字符
- 通配符函数:wildcard,用法如:$(wildcard *.o)
静态模式规则
静态模式规则的定义如下:
<targets>: <target-pattern> : <prereq-patterns ...> commands
其中:
- <targets>:文件集合
- <target-pattern>:一个模式,用以匹配<targets>中的文件
- <prereq-patterns ...>:依赖文件的模式
其中模式使用含有%的表示进行匹配。
下面是一个例子:
all: app
app: cal.o main.o
g++ -o app $^
cal.o main.o: %.o: %.cpp
g++ -c -o $@ $^
.PHONY: clean
clean:
rm -rf app *.o
其中$@和$^是独立变量,下面会讨论。
静态模式规则是一种动态规则,用以批量定义规则,批量定义目标文件、依赖文件和命令。但它不是隐式规则,所以你还是要定义目标、依赖和命令。
自动生成依赖
上面提到,结合隐式规则最好要声明一些文件之间的依赖。Make提供一种简便的方式用于自动生成每个源文件的依赖,例如main.o依赖于main.cpp和main.h,但是也没有那么方便。
我们可以使用编译器命令为中间文件批量生产依赖文件配置:中间文件依赖源文件和头文件。对于cc编译器,可以使用-M选项指定,对于GNU/GCC编译器可以使用-MM选项指定,例如gcc -MM main.c。
在Makefile中实现方式如下:
- 使用gcc -MM命令对某一个源文件生成依赖,将命令的输出保存到文件F中。
- 然后使用include将F包含到Makefile中。
Makefile变量使用
Makefile中的变量类似C中宏,变量的定义有以下4种方式:
- A=v,递归赋值,对所有与A相关的变量都受影响,即使以后更改A的值,也会影响前面与A相关的值。
- A:=v,简单赋值,类似一般编程的变量,仅对当前赋值语句有关,更改A的值只会影响以后相关的值,不会影响之前A相关的值。
- A?=v,条件赋值,如果A未定义则赋值,否则不赋值。
- A+v,追加赋值,A使用空格隔开追加新值v。
引用变量有以下两种形式:
- $(V)
- ${V}
例子如下,该例子会输出Hello World! Tom:
hello = hello
message = $(hello)
name := Tom
info = $(message) $(name)
hello = Hello World!
send := $(info)
chat:
@echo $(send)
自动变量
自动变量是make自动产生的变量:
- $@:带后缀的目标文件名
- $*:不带后缀的目标文件名
- $<:第一个依赖文件名
- $^:所有去重依赖文件名列表
- $+:所有依赖不去重文件列表
- $%:目标文件为库,代表静态库的一个成员名,如ar r $?,自动使用依赖编译目标库文件
- $?:比目标文件新的依赖文件列表
这些自动变量结合F和D又可产生新的变量,表示提出文件名的某一部分:
- F:提取文件的文件名部分,例如$(@F)获取目标文件名。
- D:提出文件的目录部分,例如$(^D)获取依赖文件的目录部分。
下面是使用自动变量的构建例子:
objects := main.o cal.o
CXX := g++
CXXFLAGS := -g
target := app
all: $(target)
$(target): $(objects)
$(CXX) -o $@ $^
$(objects): %.o: %.cpp
$(CXX) -c -o $@ $<
.PHONY: clean
clean:
-rm -rf $(target) $(objects)
变量替换
变量替换是make中一个非常有用的,它的两种形式如下:
- V:= $(vars:.a=.b),将var中的所有*.a文件替换成*.b文件,替换结果返回赋值给V。
- V:= $(var:%.a=%.b),这是一种更为灵活的方式,你可以使用它替换字符串的任何部分。
变量嵌套
在一个变量的赋值中引用其他的变量,引用变量的数量和和次数不限制,可表示变量的名称,可表示变量的值,值和名称和交互转换。
Makefile路径搜索
如果你的项目有多个不同目录的源码,或者分模块,那么这时候编译每个文件就有些麻烦了,这时可使用make提供的两种方式。
- VPATH,全部大写,它是一个环境变量,指定该变量表示搜索某一个路径下的文件,多个路径使用空格或冒号隔开,例如VPATH := src include,或者VPATH := src:include。
- vpath,全部小写,它是一个关键字,表示搜索某一个路径下的文件,并使用条件过滤,它的语法为:vpath pattern dirs。
-
- vpath %.file dir:dir2:搜索指定目录的特定文件
- vpath %.file:清除匹配文件的搜索目录(上面是添加搜索目录,这里是删除)
- vpath:清除所有已被设置的搜索目录
Makefile条件判断
条件语句用于实际执行的部分,决定某一部分是否应该执行,类似C的条件宏指令,可用于变量赋值和命令执行。
条件判断的相关指令如下:
- ifeq:判断是否相等,可使用括号,例如ifeq (A, V),或省略括号使用单引号和双引号,或者单引号和双引号混用,一下的指令类似。
- ifneq:判断是否不相等。
- ifdef A,判断A是否已经被定义。
- ifnde A,判断A是否未定义。
- else,结合if使用。
- endif:结束条件判断。
(不好意思,没时间写过多的例子了)
Makefile函数
Make提供少量的函数,调用函数的形式为:
$(<function> <arguments>)
函数和参数用空格分隔,多个参数使用逗号分隔。
首先介绍的是通配符函数:wildcard,用于在变量中展开模式,例如$(wildcard *.c)。
字符串处理函数
1、字符串模式替换
$(patsubst <pattern>,<replacement>,<text>)
查找 text 是否符合 pattern,符合用 replacement 替换
返回值为替换后的新字符串
2、字符串替换
$(subst <from>,<to>,<text>)
将字符串中的from替换成to
返回值为替换后的新字符串
3、去除空格函数
$(strip <string>)
去除首尾空格
将多个中间的连续空格合并为一个空格
返回值为去除空格后的字符串
4、字符串查找
$(findstring <find>,<in>)
查找 in 中的 find
存在返回值为目标字符串,否则为空
5、过滤函数
$(filter <pattern>,<text>)
过滤出 text 中符合模式 pattern 的字符串
可以有多个 pattern
返回值为过滤后的字符串
6、反过滤函数
$(filter-out <pattern>,<text>)
和 filter 函数正好相反,但是用法相同
7、排序函数
$(sort <list>)
list单词排序(升序)
sort会去除重复的字符串
返回值为排列后的字符串
8、取单词函数
$(word <n>,<text>)
取出<text>中的第n个单词
返回值为第 n 个单词
文件名操作函数
1、取目录函数
$(dir <names>)
names 中取出目录部分
2、取文件函数
$(notdir <names>)
names 中取出非目录的部分
3、取后缀名函数
$(suffix <names>)
names 中取出各个文件的后缀名
4、取前缀函数
$(basename <names>)
names 中取出各个文件名的前缀部分
5、添加后缀名函数
$(addsuffix <suffix>,<names>)
把后缀 suffix 加到 names 中的每个单词后面
6、添加前缀名函数
$(addperfix <prefix>,<names>)
把前缀 prefix 加到 names 中的每个单词的前面
7、链接函数
$(join <list1>,<list2>)
list2 中的单词对应的拼接到 list1 的后面
一一对应连接,多出部分不拼接
8、获取匹配模式文件名函数
$(wildcard PATTERN)
通配符函数
列出当前目录符合模式PATTERN 的文件名
其它常用函数
1、foreach函数
$(foreach <var>,<list>,<text>)
把参数<list>中的单词逐一取出放到参数<var>所指定的变量中
然后再执行<text>所包含的表达式
返回值:<text>所返回的每个字符串所组成的字符串(以空格分隔)
2、if函数
$(if <condition>,<then-part>)
(if<condition>,<then-part>,<else-part>)
condition返回非空字符串,则为真
3、call函数
$(call <expression>,<parm1>,<parm2>,<parm3>,...)
函数定义
expression = $(1)、$(2)、$(3)......
$(call expression,a,b)
返回值为expression的值
4、origin函数
$(origin <variable>)
告诉变量来自哪里
variable 是变量的名称
按变量来源分类返回,分类如下:
undefined
default
environment
file
command line
override
automatic
5、shell函数
$(shell shell-command)
用于执行shell命令
控制函数
1、error函数
$(error TEXT...)
产生致命错误,并提示 "TEXT..." 信息
退出 make 的执行
2、warning函数
$(warning TEXT...)
不会导致致命错误
提示 "TEXT..."
继续执行
Makefile命令参数
- -b,-m
-
- 忽略
- -B,--always-make
-
- 不跟据依赖描述,强制重新构建所有规则目标
- -C DIR,--directory=DIR
-
- 读取 Makefile 之前,进入到目录 DIR,然后执行 make
- 多个-C,第一个目录为最终工作目录
- -d
-
- 执行的过程中打印出所有的调试信息
- --debug[=OPTIONS]
-
- make 执行时输出调试信息
- OPTIONS默认为b
- OPTIONS值,首字母有效
-
- all、basic、verbose、implicit、jobs、makefile
- -e,--enveronment / -overrides
-
- 使用环境变量定义覆盖 Makefile 中的同名变量定义
- -f=FILE,--file=FILE,
- --makefile=FILE
-
- 指定文件 "FILE" 为 make 执行的 Makefile 文件
- -p,--help
-
- 打印帮助信息
- -i,--ignore-errors
-
- 忽略规则命令执行的错误。
- -I DIR,--include-dir=DIR
-
- 指定包含 Makefile 文件的搜索目录
- -j [JOBS],--jobs[=JOBS]
-
- 可指定同时执行的命令数目
- -k,--keep-going
-
- 执行命令错误时不终止 make 的执行
- -l load,--load-average=[=LOAD],--max-load[=LOAD]
-
- 告诉 make 在存在其他任务执行的时候,如果系统负荷超过 "LOAD",不在启动新的任务
- -n,--just-print,--dry-run
-
- 只打印执行的命令,但是不执行命令。
- -o FILE,--old-file=FILE,--assume-old=FILE
-
- 指定 "FILE"文件不需要重建,即使是它的依赖已经过期;同时不重建此依赖文件的任何目标
- -p,--print-date-base
-
- 打印出 make 读取的 Makefile 的所有数据
- -q,-question
-
- 称为 "询问模式" ;不运行任何的命令,并且无输出
- -r,--no-builtin-rules
-
- 取消所有的内嵌函数的规则
- -R,--no-builtin-variabes
-
- 取消 make 内嵌的隐含变量
- -s,--silent,--quiet
-
- 取消命令执行过程中的打印。
- -S,--no-keep-going,
- --stop
-
- 取消 "-k" 的选项在递归的 make 过程中子 make 通过 "MAKEFLAGS" 变量继承了上层的命令行选项那个
- -t,--touch
-
- 更新所有的目标文件的时间戳到当前系统时间。防止 make 对所有过时目标文件的重建
- -v,version
-
- 查看make的版本信息
- -w,--print-directory
-
- 在 make 进入一个子目录读取 Makefile 之前打印工作目录
- --no-print-directory
-
- 取消 "-w" 选项
- -W FILE,--what-if=FILE,
- --new-file=FILE,
- --assume-file=FILE
-
- 设定文件 "FILE" 的时间戳为当前的时间,但不更改文件实际的最后修改时间
- 用于强制重建
- --warn-undefined-variables
-
- 在发现 Makefile 中存在没有定义的变量进行引用时给出告警信息
多个Makefile:多模块项目构建
文件包含
使用语法如下:
include <filenames>
在Makefile中包含其它文件,使用关键字include。Filenames:shell 支持的文件名,可以使用通配符表示。
读入被包含的文件忽略行首空格,多个连续包含使用空格分开。
使用场合:
- 包含通用的Makefile文件:包含通用的变量和模式规则
- 将自动产生的依赖保存到另一个文件中
使用-include代替include:忽略文件不存在或者是无法创建的错误提示。
搜索文件顺序:
- 从include指定的文件查找
- 从make命令行指定的路径查找,-I或--include-dir
- 查找系统默认路径:"usr/gnu/include"、"usr/local/include" 和 "usr/include"
嵌套执行make
一般来说,开发一个项目我们需要分模块,或者说我们需要分部分来编译。当然非常小的项目就没必要,但是多模块开发的Makefile才适合普遍情况。
多模块开发需要在不同的模块中使用自己的Makefile,整体Makefile部署如下:
- 根目录使用一个Makefile作为整体控制。
- 每个子模块或子项目使用自己的Makefile。
- 父模块中使用shell命令先去执行子模块的构建,命令方式如下:
-
- cd subdir && $(MAKE),其实就是和平时使用shell一样,先进入根目录,然后执行make命令。
- $(MAKE) -C subdir,-C选项进入指定的make工作目录执行,make工作目录保存在CURDIR变量中。
导出变量和Export
在多个Makefile构建中,如果要向下传递变量,可使用:
export <variable>
如果所有变量都需要传递,仅仅使用export即可。
如果不需要传递任何变量可使用:unexport <variable>。
必定传递的变量有SHELL和MAKEFLAGS,去过需要取消传递,可在make命令中添加例如MAKEFLAGS=。
不传递的参数选项包括:"-C"、"-f"、"-o"、"-h" 和 "-W"。
多模块构建
多模块构建包括两个任务:
- 在根Makefile中处理多个模块,使用make -C完成。
- 处理模块之间的依赖关系,例如$(A): $(B)。
要注意的是,多模块的项目,任意两个模块建议不要有父子目录的关系,例如模块app目录中包含所有的lib目录,这会引起麻烦。建议将所有的模块在根目录中平行放置,或只要它们的目录没有后代关系即可。
多项目构建模板:Makefile完整配置
这里介绍一个多模块构建的相对规范的Makefile配置,A/B/C模块都是子模块,编译成库,app是应用程序,其中标准项目结构如下:
.
├── A
│ ├── build
│ ├── include
│ ├── lib
│ ├── Makefile
│ ├── sources
│ └── src
├── app
│ ├── build
│ ├── include
│ ├── lib
│ ├── Makefile
│ ├── sources
│ └── src
├── B
│ ├── build
│ ├── include
│ ├── lib
│ ├── Makefile
│ ├── sources
│ └── src
├── C
│ ├── build
│ ├── include
│ ├── lib
│ ├── Makefile
│ ├── sources
│ └── src
├── Makefile
└── readme.txt
其中:
- Build:保存构建产生的文件,主要是中间文件。
- Include:保存项目的头文件,包括当前项目的头文件和库头文件。
- Lib:保存项目使用到的库文件,包括第三方库和当前模块生成的库。
- Source:保存项目使用到的资源文件。
- Src:保存项目的源码。
针对该项目,根Makefile的配置如下:
MAKE := make
APP := App # 应用程序
RUN := app.run
A := A # 辅助模块, 如UI
B := B # 辅助模块, 如数据库
C := C # 辅助模块, 如播放器
Models := $(A) $(B) $(C)
LIBS := $(foreach n,$(Models),$(n)/lib/lib$(n).so)
LOCALLIBS := $(foreach n,$(Models),usr/lib/lib$(n).so)
# 声明伪目标
.PHONY: all install clean $(APP) $(Models)
all: install
# 声明模块依赖
$(APP): $(Models)
$(A): $(B) $(C)
# 每个模块的make
$(APP) $(Models):
$(MAKE) -C $@
install: $(APP)
sudo cp $(LIBS) /usr/lib
sudo cp $(strip $(APP))/build/App $(RUN)
clean:
$(MAKE) $@ -C $(APP)
$(MAKE) $@ -C $(A)
$(MAKE) $@ -C $(B)
$(MAKE) $@ -C $(C)
sudo rm -rf $(LOCALLIBS)
rm -rf $(RUN)
app的Makefile配置如下:
VPATH := src include
CXX := g++
CXXFLAGS := -g
BUILD := build
SRC := src
INCLUDE := include
LIB := lib
TARGET := App
OBJECTS := main.o
FULLOBJECTS := $(foreach n,$(OBJECTS),$(BUILD)/$(n))
MODELNAMES := A B C
MODELHEADERPATH := $(foreach n,$(MODELNAMES),-I../$(n)/include)
MODELSONAME := $(foreach n,$(MODELNAMES),-L../$(n)/lib -l$(n))
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(FULLOBJECTS)
$(CXX) -o $(BUILD)/$@ $^ -L$(LIB) $(MODELSONAME)
$(FULLOBJECTS): $(OBJECTS)
$(OBJECTS): %.o: %.cpp
$(CXX) -fPIC -c -o $(BUILD)/$@ $< -I$(INCLUDE) $(MODELHEADERPATH)
clean:
-rm -rf $(BUILD)/**
子模块A的Makefile配置如下:
VPATH := src include
CXX := g++
CXXFLAGS := -g
BUILD := build
SRC := src
INCLUDE := include
LIB := lib
TARGET := A
OBJECTS := controller.o
FULLOBJECTS := $(foreach n,$(OBJECTS),$(BUILD)/$(n))
NAME := $(LIB)/lib$(TARGET).so
MODELNAMES := B C
MODELHEADERPATH := $(foreach n,$(MODELNAMES),-I../$(n)/include)
MODELSONAME := $(foreach n,$(MODELNAMES),-L../$(n)/lib -l$(n))
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(FULLOBJECTS)
$(CXX) -shared -o $(NAME) $^ -L$(LIB) $(MODELSONAME)
$(FULLOBJECTS): $(OBJECTS)
$(OBJECTS): %.o: %.cpp
$(CXX) -fPIC -c -o $(BUILD)/$@ $< -I$(INCLUDE) $(MODELHEADERPATH)
clean:
-rm -rf $(BUILD)/** $(NAME)
子模块B的Makefile配置如下:
VPATH := src include
CXX := g++
CXXFLAGS := -g
BUILD := build
SRC := src
INCLUDE := include
LIB := lib
TARGET := B
OBJECTS := cal.o
FULLOBJECTS := $(foreach n,$(OBJECTS),$(BUILD)/$(n))
NAME := $(LIB)/lib$(TARGET).so
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(FULLOBJECTS)
$(CXX) -shared -o $(NAME) $^ -L$(LIB) # -l<libname>
$(FULLOBJECTS): $(OBJECTS)
$(OBJECTS): %.o: %.cpp
$(CXX) -fPIC -c -o $(BUILD)/$@ $< -I$(INCLUDE)
clean:
-rm -rf $(BUILD)/** $(NAME)
子模块C的Makefile配置如下:
VPATH := src include
CXX := g++
CXXFLAGS := -g
BUILD := build
SRC := src
INCLUDE := include
LIB := lib
TARGET := C
OBJECTS := print.o
FULLOBJECTS := $(foreach n,$(OBJECTS),$(BUILD)/$(n))
NAME := $(LIB)/lib$(TARGET).so
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(FULLOBJECTS)
$(CXX) -shared -o $(NAME) $^ -L$(LIB) # -l<libname>
$(FULLOBJECTS): $(OBJECTS)
$(OBJECTS): %.o: %.cpp
$(CXX) -fPIC -c -o $(BUILD)/$@ $< -I$(INCLUDE)
clean:
-rm -rf $(BUILD)/** $(NAME)
以上配置中,A/B/C模块的Makefile是类似的。
总结
到这里Makefile的编写学习基本完成了,本文讨论的内容包括:
- Makefile的构建任务,知道构建任务是什么才能更好编写构建脚本。
- Makefile的规则,包括规则的基本结构,显示规则的编写和隐式规则的使用,其中有两个难点是:静态模式规则和自动生成依赖。
- Makefile变量,包括4种变量定义形式,还有常用的自动变量,变量替换是一个非常好用的功能。
- 路径搜索,使用VPATH和vpath在一个模块中搜索所有需要的文件。
- 条件判断,用于选择性执行某一部分的代码。
- Makefile的函数,包括通配符函数wildcard,字符串处理函数,文件名操作函数,控制函数,以及其它一些常用函数。
- Make命令参数,可以使用man详细查看。
- Makefile的多模块构建,其实就是对前面学习的内容的综合。
- 最后是Makefile多模块构建配置的模板,该模板适用于多数项目配置。
最后最后,Makefile编写值得我们花几天学完,但是真的项目构建,我觉得还是使用cmake比较好,只是cmake也是基于makefile的,不懂makefile,若出现什么问题,也是很头痛,而且几天时间也不算多吧,还是值得的。