Makefile文件编写完整教程和实例分析

2021年3月8日16:27:03 发表评论 1,085 次浏览

本文概述

前言

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,若出现什么问题,也是很头痛,而且几天时间也不算多吧,还是值得的。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: