Makefile学习总结

对于Makefile学习的总结,同时包含了其他人的总结。

学习环境搭建

Linux(以Ubuntu为例)

1
sudo apt install gcc g++ make

Windows

学习与演示过程以Windows为主,Windows上装MinGW环境,MinGW官网: https://www.mingw-w64.org/
之前我们提过两个版本的环境,MingW-W64-buildsw64devkit
推荐使用w64devkit套件,里面工具比较齐全,还提供模拟了许多Linux命令,用这个套件环境来学习可以保持在Linux与Windows上Makefile书写方式一致。
以下是w64devkit与其他包一些命令的区别

w64devkit(模拟Linux) MingW-W64-builds或其他套件(Windows cmd命令)
make mingw32-make
cc gcc
rm del
touch
ls dir
sh
mv
cp copy/xcopy
sed

 

学习材料

make官方文档: https://www.gnu.org/software/make/manual/make.html

2048: https://github.com/plibither8/2048.cpp

sudoku: https://github.com/mayerui/sudoku
 
 
 
 

Makefile基础知识

make使用流程

  1. 准备好需要编译的源代码
  2. 编写Makefile文件
  3. 在命令行执行make命令

 
 

最简单的Makefile

1
2
hello: hello.cpp
g++ hello.cpp -o hello # 开头必须为一个Tab,不能为空格

但通常需要将编译与链接分开写,分为如下两步

1
2
3
4
hello: hello.o
g++ hello.o -o hello
hello.o: hello.cpp
g++ -c hello.cpp

 

规则(Rules):一个Makefile文件由一条一条的规则构成,一条规则结构如下

1
2
3
4
target … (目标): prerequisites …(依赖)
recipe(方法)


第二种写法

1
target … (目标): prerequisites …(依赖); recipe(方法) ;…

 
 
Make主要用于处理C和C++的编译工作,但不只能处理C和C++,所有编译器/解释器能在命令行终端运行的编程语言都可以处理(例如Java、Python、 Golang….)。Make也不只能用来处理编程语言,所有基于一些文件(依赖)的改变去更新另一些文件(目标)的工作都可以做。

Make编译与打包Java程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
snake.jar : C.class Main.class SnakeFrame.class SnakePanel.class
jar -cvfe snake.jar Main *.class

C.class : C.java
javac C.java

Main.class : Main.java
javac Main.java

SnakeFrame.class : SnakeFrame.java
javac SnakeFrame.java

SnakePanel.class : SnakePanel.java
javac SnakePanel.java

.PHONY: clean
clean:
rm *.class *.jar

 
 
 
 
 

Makefile文件的命名与指定

Make会自动查找makefile文件,查找顺序为GNUmakefile -> makefile -> Makefile

GNUmakefile:不建议使用,因为只有GNU make会识别,其他版本的make(如BSD make, Windows nmake等)不会识别,如果只给GNU make使用的情况

makefile:可以使用,GNU make和其他版本make识别

Makefile:最常用,强烈建议使用

如果运行make的时候没有找到以上名字的文件,则会报错,这时候可以手动指定文件名

1
2
make -f mkfile  # make -f <filename>
make --file=mkfile # make --file=<filename>

手动指定之后,make就会使用指定的文件,即使有Makefile或者makefile不会再自动使用

 
 
 

Makefile文件内容组成

一个Makefile文件通常由五种类型的内容组成:显式规则、隐式规则、变量定义、指令和注释

显式规则(explicit rules):显式指明何时以及如何生成或更新目标文件,显式规则包括目标、依赖和更新方法三个部分

隐式规则(implicit rules):根据文件自动推导如何从依赖生成或更新目标文件。

变量定义(variable definitions):定议变量并指定值,值都是字符串,类似C语言中的宏定义(#define),在使用时将值展开到引用位置

指令(directives):在make读取Makefile的过程中做一些特别的操作,包括:

  1. 读取(包含)另一个makefile文件(类似C语言中的#include)

  2. 确定是否使用或略过makefile文件中的一部分内容(类似C语言中的#if)

  3. 定义多行变量

注释(comments):一行当中 # 后面的内容都是注释,不会被make执行。make当中只有单行注释。如果需要用到#而不是注释,用\#。

 
 
 
 

 

 

一个稍微复杂的Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sudoku: block.o command.o input.o main.o scene.o test.o
g++ -o sudoku block.o command.o input.o main.o scene.o test.o

block.o: block.cpp common.h block.h color.h
g++ -c block.cpp

command.o: command.cpp scene.h common.h block.h command.h
g++ -c command.cpp

input.o: input.cpp common.h utility.inl
g++ -c input.cpp

main.o: main.cpp scene.h common.h block.h command.h input.h
g++ -c main.cpp

scene.o: scene.cpp common.h scene.h block.h command.h utility.inl
g++ -c scene.cpp

test.o: test.cpp test.h scene.h common.h block.h command.h
g++ -c test.cpp

hello.o: hello.cpp
g++ -c hello.cpp


clean:
rm block.o command.o input.o main.o scene.o test.o
rm sudoku.exe
1
2
3
4
target … (目标): prerequisites …(依赖)
recipe(方法)


目标

  1. Makefile中会有很多目标,但最终目标只有一个,其他所有内容都是为这个最终目标服务的,写Makefile的时候先写出最终目标,再依次解决总目标的依赖

  2. 一般情况第一条规则中的目标会被确立为最终目标,第一条规则默认会被make执行

  3. 通常来说目标是一个文件,一条规则的目的就是生成或更新目标文件。

  4. make会根据目标文件和依赖文件最后修改时间判断是否需要执行更新目标文件的方法。如果目标文件不存在或者目标文件最后修改时间早于其中一个依赖文件最后修改时间,则重新执行更新目标文件的方法。否则不会执行。

  5. 除了最终目标对应的更新方法默认会执行外,如果Makefile中一个目标不是其他目标的依赖,那么这个目标对应的规则不会自动执行。需要手动指定,方法为

    1
    make <target>  # 如 make clean , make hello.o
  6. 可以使用.DEFAULT_GOAL来修改默认最终目标

    1
    2
    3
    4
    5
    6
    7
    .DEFAULT_GOAL = main

    all:
    @echo all

    main:
    @echo main

 

 

 

 

伪目标

如果一个标并不是一个文件,则这个目标就是伪目标。例如前面的clean目标。如果说在当前目录下有一个文件名称和这个目标名称冲突了,则这个目标就没法执行。这时候需要用到一个特殊的目标 .PHONY,将上面的clean目标改写如下

1
2
3
4
.PHONY: clean
clean:
rm block.o command.o input.o main.o scene.o test.o
rm sudoku.exe

这样即使当前目录下存在与目标同名的文件,该目标也能正常执行。

伪目标的其他应用方式

如果一条规则的依赖文件没有改动,则不会执行对应的更新方法。如果需要每次不论有没有改动都执行某一目标的更新方法,可以把对应的目标添加到.PHONY的依赖中,例如下面这种方式,则每次执行make都会更新test.o,不管其依赖文件有没有改动

1
2
3
4
test.o: test.cpp test.h
g++ -c test.cpp

.PHONY: clean test.o

 

 

 

 

 

 

依赖

依赖类型

普通依赖

前面说过的这种形式都是普通依赖。直接列在目标后面。普通依赖有两个特点:

  1. 如果这一依赖是由其他规则生成的文件,那么执行到这一目标前会先执行生成依赖的那一规则
  2. 如果任何一个依赖文件修改时间比目标晚,那么就重新生成目标文件

order-only依赖

依赖文件不存在时,会执行对应的方法生成,但依赖文件更新并不会导致目标文件的更新

如果目标文件已存在,order-only依赖中的文件即使修改时间比目标文件晚,目标文件也不会更新。

定义方法如下:

1
targets : normal-prerequisites | order-only-prerequisites

normal-prerequisites部分可以为空

 

 

 

 

 

指定依赖搜索路径

make默认在Makefile文件所在的目录下查找依赖文件,如果找不到,就会报错。这时候就需要手动指定搜索路径,用VPATH变量或vpath指令。

VPATH用法如下:

1
2
3
VPATH = <dir1>:<dir2>:<dir3>...
# 例如
VPATH = include:src

多个目录之间冒号隔开,这时make会在VPATH指定的这些目录里面查找依赖文件。

vpath指令用法:

vpath比VPATH使用更灵活,可以指定某个类型的文件在哪个目录搜索。

用法如下:

1
2
3
4
5
6
7
8
vpath <pattern> <directories>

vpath %.h include # .h文件在include目录下查找
vpath %.h include:headers # .h文件在include或headers文件下查找

vpath % src # 所有文件都在src下查找

vpath hello.cpp src # hello.cpp文件在src查找

 

 

 

 

更新方法

1
2
3
4
target … (目标): prerequisites …(依赖)
recipe(方法)


关于执行终端

更新方法实际上是一些Shell指令,通常以Tab开头,或直接放在目标-依赖列表后面,用分号隔开。这些指令都需要交给Shell执行,所以需要符合Shell语法。默认使用的Shell是sh,在Windows上如果没有安装sh.exe的话会自动查找使用cmd.exe之类的终端。这时有的指令写法,例如循环语句,与Linux不同,需要注意。

可以通过SHELL变量手动指定Shell

1
2
SHELL = C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe
SHELL = cmd.exe

默认的执行方式为一条指令重新调用一个Shell进程来执行。有时为了提高性能或其他原因,想让这个目标的所有指令都在同一进程中执行,可以在Makefile中添加 .ONESHELL

1
.ONESHELL:

这样所有指令都会在同一次Shell调用中执行

Shell语句回显问题

通常make在执行一条Shell语句前都会先打印这条语句,如果不想打印可以在语句开头在@

1
2
@echo hello
@g++ -o hello hello.cpp

也可以使用.SILENT来指定哪些目标的更新方法指令不用打印

1
.SILENT: main all

错误处理

如果一条规则当中包含多条Shell指令,每条指令执行完之后make都会检查返回状态,如果返回状态是0,则执行成功,继续执行下一条指令,直到最后一条指令执行完成之后,一条规则也就结束了。

如果过程中发生了错误,即某一条指令的返回值不是0,那么make就会终止执行当前规则中剩下的Shell指令。

例如

1
2
3
clean:
rm main.o hello.o
rm main.exe

这时如果第一条rm main.o hello.o出错,第二条rm main.exe就不会执行。类似情况下,希望make忽视错误继续下一条指令。在指令开头-可以达到这种效果。

1
2
3
clean:
-rm main.o hello.o
-rm main.exe

 

 

 

 

 

 

变量应用

Makefile中的变量有点类似C语言中的宏定义,即用一个名称表示一串文本。但与C语言宏定义不同的是,Makefile的变量值是可以改变的。变量定义之后可以在目标、依赖、方法等Makefile文件的任意地方进行引用。

Makefile中的变量值只有一种类型: 字符串

变量可以用来表示什么

  • 文件名序列

  • 编译选项

  • 需要运行的程序

  • 需要进行操作的路径

  • ……

变量定义与引用方式

定义方式

1
2
3
4
# <变量名> = <变量值>  <变量名> := <变量值>  <变量名> ::= <变量值>
files = main.cpp hello.cpp
objects := main.o hello.o
var3 ::= main.o

变量名区分大小写,可以是任意字符串,不能含有”:”, “#”, “=”

使用方式

1
2
3
# $(<变量名>) 或者 ${<变量名>}
main.o : $(files) # 或者 ${files}
...

如果变量名只有一个字符,使用时可以不用括号,如$a, $b, 但不建议这样用,不管是否只有一个字符都写成$(a), $(b)这种形式

Makefile读取过程

GNU make分两个阶段来执行Makefile,第一阶段(读取阶段):

  • 读取Makefile文件的所有内容

  • 根据Makefile的内容在程序内建立起变量

  • 在程序内构建起显式规则、隐式规则

  • 建立目标和依赖之间的依赖图

第二阶段(目标更新阶段):

  • 用第一阶段构建起来的数据确定哪个目标需要更新然后执行对应的更新方法

变量和函数的展开如果发生在第一阶段,就称作立即展开,否则称为延迟展开。立即展开的变量或函数在第一个阶段,也就是Makefile被读取解析的时候就进行展开。延迟展开的变量或函数将会到用到的时候才会进行展开,有以下两种情况:

  • 在一个立即展开的表达式中用到

  • 在第二个阶段中用到

显式规则中,目标和依赖部分都是立即展开,在更新方法中延迟展开

 

 

 

 

 

 

变量赋值

递归展开赋值(延迟展开)

第一种方式就是直接使用=,这种方式如果赋值的时候右边是其他变量引用或者函数调用之类的,将不会做处理,直接保留原样,在使用到该变量的时候再来进行处理得到变量值(Makefile执行的第二个阶段再进行变量展开得到变量值)

1
2
3
4
5
6
7
8
9
10
11
12
bar2 = ThisIsBar2No.1
foo = $(bar)
foo2 = $(bar2)

all:
@echo $(foo) # Huh?
@echo $(foo2) # ThisIsBar2No.2
@echo $(ugh) # Huh?

bar = $(ugh)
ugh = Huh?
bar2 = ThisIsBar2No.2

简单赋值(立即展开)

简单赋值使用:=::=,这种方式如果等号右边是其他变量或者引用的话,将会在赋值的时候就进行处理得到变量值。(Makefile执行第一阶段进行变量展开)

1
2
3
4
5
6
7
8
9
10
11
12
bar2 := ThisIsBar2No.1
foo := $(bar)
foo2 := $(bar2)

all:
@echo $(foo) # 空串,没有内容
@echo $(foo2) # ThisIsBar2No.1
@echo $(ugh) #

bar := $(ugh)
ugh := Huh?
bar2 := ThisIsBar2No.2

条件赋值

条件赋值使用?=,如果变量已经定义过了(即已经有值了),那么就保持原来的值,如果变量还没赋值过,就把右边的值赋给变量。

1
2
3
4
5
var1 = 100
var1 ?= 200

all:
@echo $(var1) # 100 注释var1 = 100之后为200

练习:试求a的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
x = hello
y = world
a := $(x)$(y)

x = y
y = z
a := $($(x))

x = y
y = z
z = u
a := $($($(x)))

x = $(y)
y = z
z = Hello
a := $($(x))

追加

使用+=在变量已有的基础上追加内容

1
2
3
4
5
files = main.cpp
files += hello.cpp

all:
@echo $(files)

Shell运行赋值

使用!=,运行一个Shell指令后将返回值赋给一个变量

1
2
gcc_version != gcc --version
files != ls .

如果使用Windows需要注意,这种赋值方式只适用于与Linux相同的Shell指令,Windows独有的指令不能这样使用。

 

 

 

 

定义多行变量

前面定义的变量都是单行的。

变量值有多行,多用于定义shell指令

语法

1
2
3
4
5
6
7
8
9
10
11
define <varable_name>  # 默认为 = 
# 变量内容
endef

define <varable_name> :=
# 变量内容
endef

define <varable_name> +=
# 变量内容
endef

 

示例

1
2
3
4
5
6
7
8
9
10
11
12
echosomething = @echo This is the first line

define echosomething +=

@echo hello
@echo world
@echo 3
endef


all:
$(echosomething)

取消变量

如果想清除一个变量,用以下方法

1
undefine <变量名>   如 undefine files,  undefine objs

环境变量的使用

系统中的环境变量可以直接在Makefile中直接使用,使用方法跟普通变量一样

1
2
3
4
all:
@echo $(USERNAME)
@echo $(JAVA_HOME)
@echo $(SystemRoot)

变量替换引用

语法:__$(var:a=b)__,意思是将变量var的值当中每一项结尾的a替换为b,直接上例子

1
2
3
4
files = main.cpp hello.cpp
objs := $(files:.cpp=.o) # main.o hello.o
# 另一种写法
objs := $(files:%.cpp=%.o)

变量覆盖

所有在Makefile中的变量,都可以在执行make时能过指定参数的方式进行覆盖。

1
2
3
OverridDemo := ThisIsInMakefile
all:
@echo $(OverridDemo)

如果直接执行

1
make

则上面的输出内容为ThisIsInMakefile,但可以在执行make时指定参数:

1
2
3
make OverridDemo=ThisIsFromOutShell # 等号两边不能有空格
# 如果变量值中有空格,需要用引号
make OverridDemo=“This Is From Out Shell”

则输出OverridDemo的值是ThisIsFromOutShell或This Is From Out Shell。

用这样的命令参数会覆盖Makefile中对应变量的值,如果不想被覆盖,可以在变量前加上override指令,override具有较高优先级,不会被命令参数覆盖

1
2
3
override OverridDemo := ThisIsInMakefile
all:
@echo $(OverridDemo)

这样即使命令行指定参数

1
make OverridDemo=ThisIsFromOutShell

输出结果依然是ThisIsInMakefile

 

 

 

 

 

自动变量

**$@**:①本条规则的目标名;②如果目标是归档文件的成员,则为归档文件名;③在多目标的模式规则中, 为导致本条规则方法执行的那个目标名;

**$<**:本条规则的第一个依赖名称

**$?**:依赖中修改时间晚于目标文件修改时间的所有文件名,以空格隔开

$^:所有依赖文件名,文件名不会重复,不包含order-only依赖

**$+**:类似上一个, 表示所有依赖文件名,包括重复的文件名,不包含order-only依赖

**$|**:所有order-only依赖文件名

__$*__:(简单理解)目标文件名的主干部分(即不包括后缀名)

**$%**:如果目标不是归档文件,则为空;如果目标是归档文件成员,则为对应的成员文件名

 

以下变量对应上述变量,D为对应变量所在的目录,结尾不带/,F为对应变量除去目录部分的文件名

$(@D)

$(@F)

$(*D)

$(*F)

$(%D)

$(%F)

$(<D)

$(<F)

$(^D)

$(^F)

$(+D)

$(+F)

$(?D)

$(?F)

 

 

 

 

 

绑定目标的变量

Makefile中的变量一般是全局变量。也就是说定义之后在Makefile的任意位置都可以使用。但也可以将变量指定在某个目标的范围内,这样这个变量就只能在这个目标对应的规则里面保用

语法

1
2
3
4
target … : variable-assignment
target … : prerequisites
recipes

1
2
3
4
5
6
7
8
9
10
11
12
var1 = Global Var

first: all t2

all: var2 = Target All Var
all:
@echo $(var1)
@echo $(var2)

t2:
@echo $(var1)
@echo $(var2)

这种定义变量的方式,目标也可以使用模式匹配,这样所有能匹配上的目标范围内都可以使用这些变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var1 = Global Var

first: all.v t2.v t3

%.v: var2 = Target %.v Var
all.v:
@echo $@ -- $(var1)
@echo $@ -- $(var2)

t2.v:
@echo $@ -- $(var1)
@echo $@ -- $(var2)
t3:
@echo $@ -- $(var1)
@echo $@ -- $(var2)

 

 

 

 

 

二次展开

前面说过依赖中的变量都是在Makefile读取阶段立即展开的。如果想让依赖的的变量延迟展开,可以使用.SECONDEXPANSION:,添加之后,在依赖中使用变量时用$$,可以让变量在第二阶段进行二次展开,从而达到延迟展开的效果。

1
2
3
4
5
6
VAR1 = main.cpp
.SECONDEXPANSION:
all: $$(VAR1)
@echo $^

VAR1 = hello.cpp

 

 

 

 

 

 

 

 

 

自动推导与隐式规则

Makefile中有一些生成目标文件的规则使用频率非常高,比如由.c或.cpp文件编译成.o文件,这样的规则在make中可以自动推导,所以可以不用明确写出来,这样的规则称为隐式规则。

一些make预定义的规则

C语言编译

从.c到.o

1
$(CC) $(CPPFLAGS) $(CFLAGS) -c

C++编译

从.cc .cpp .C到.o

1
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c

链接

由.o文件链接到可执行文件

1
$(CC) $(LDFLAGS) *.o $(LOADLIBES) $(LDLIBS)

隐式规则中常用一些变量

这些变量都有默认值,也可以自行修改

CC

编译C语言的程序,默认为 cc

CXX

编译C++的程序,默认为 g++

AR

归档程序,默认为 ar

CPP

C语言预处理程序,默认为 $(CC) -E

RM

删除文件的程序,默认为rm -f

CFLAGS

传递给C编译器的一些选项,如-O2 -Iinclude

CXXFLAGS

传递给C++编译器的一些选项,如-std=c++ 11 -fexec-charset=GBK

CPPFLAGS

C语言预处理的一些选项

LDFLAGS

链接选项,如-L.

LDLIBS

链接需要用到的库,如-lkernel32 -luser32 -lgdi32

 

 

 

 

 

 

 

多目标与多规则

显式规则中一条规则可以有多个目标,多个目标可以是相互独立的目标,也可以是组合目标,用写法来区分

独立多目标

相互独立的多个目标与依赖之间直接用:,常用这种方式的有以下两种情况

  1. 只需要写目标和依赖,不需要写方法的时候

    1
    block.o input.o scene.o : common.h

    这种写法等价于

    1
    2
    3
    block.o : common.h
    input.o : common.h
    scene.o : common.h
  2. 生成(更新)目标的方法写法一样的,只是依赖与目标不一样时。之前写的Makfile中,有如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    block.o: block.cpp common.h block.h color.h
    g++ -c block.cpp
    command.o: command.cpp command.h scene.h
    g++ -c command.cpp
    input.o: input.cpp common.h utility.inl
    g++ -c input.cpp
    main.o: main.cpp scene.h input.h test.h
    g++ -c main.cpp
    scene.o: scene.cpp common.h scene.h utility.inl
    g++ -c scene.cpp
    test.o: test.cpp test.h
    g++ -c test.cpp

    所有.o文件的生成都用的同一方法

    1
    g++ -c <文件名>

    如果不考虑依赖源文件进行更新时,可以进行简写如下:

    1
    2
    block.o command.o input.o main.o scene.o test.o : common.h block.h command.h ...
    g++ -c $(@:%.o=%.cpp)

    这种写法实际上等价于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    block.o : common.h block.h command.h ...
    g++ -c $(subst .o,.cpp,$@)
    command.o : common.h block.h command.h ...
    g++ -c $(subst .o,.cpp,$@)
    input.o : common.h block.h command.h ...
    g++ -c $(subst .o,.cpp,$@)
    main.o : common.h block.h command.h ...
    g++ -c $(subst .o,.cpp,$@)
    scene.o : common.h block.h command.h ...
    g++ -c $(subst .o,.cpp,$@)
    test.o : common.h block.h command.h ...
    g++ -c $(subst .o,.cpp,$@)

    其中,$@表示的是目标名称。subst是一个字符串替换函数,$(subst .o,.cpp,$@)表示将目标名称中的.o替换为.cpp。

    这样的简写可以减少内容的书写量,但是不利于将每个目标与依赖分别对应。

独立多目标虽然写在一起,但是每个目标都是单独调用一次方法来更新的。和分开写效果一样。

组合多目标

多目标与依赖之前用&:,这样的多个目标称为组合目标。与独立多目标的区别在于,独立多目标每个目标的更新需要单独调用一次更新方法。而组合多目标调用一次方法将更新所有目标

1
2
3
4
block.o input.o scene.o &: block.cpp input.cpp scene.cpp common.h
g++ -c block.cpp
g++ -c input.cpp
g++ -c scene.cpp

所有目标的更新方法都写到其中,每次更新只会调用一次。

同一目标多条规则

同一目标可以对应多条规则。同一目标的所有规则中的依赖会被合并。但如果同一目标对应的多条规则都写了更新方法,则会使用最新的一条更新方法,并且会输出警告信息。

同一目标多规则通常用来给多个目标添加依赖而不用改动已写好的部分。

1
2
3
4
5
6
7
8
input.o: input.cpp utility.inl
g++ -c input.cpp
main.o: main.cpp scene.h input.h test.h
g++ -c main.cpp
scene.o: scene.cpp scene.h utility.inl
g++ -c scene.cpp

input.o main.o scene.o : common.h

同时给三个目标添加了一个依赖common.h,但是不用修改上面已写好的部分。

静态模式

独立多目标可以简化Makefile的书写,但是不利于将各个目标的依赖分开,让目标文件根据各自的依赖进行更新。静态模式可以在一定程度上改进依赖分开问题。

静态模式就是用%进行文件匹配来推导出对应的依赖。

语法

1
2
3
targets …: target-pattern(目标模式): prereq-patterns(依赖模式) …
recipe

先看一个例子

1
2
block.o : %.o : %.cpp %.h
g++ -c $<

block.o为目标,%.o为目标模式,%.cpp,%.h为依赖模式,对于这一条规则,%.o代表的是目标文件block.o,所以这里的%匹配的是block,因此,%.cpp表示block.cpp,%.h代表block.h,所以block.o : %.o : %.cpp %.h表示的意思同下面这种写法

1
block.o : block.cpp block.h

自动推导出block.o依赖block.cpp和block.h。

另外,$<表示目标的第一个依赖,在这条规则中,$<表示block.cpp

对应的Makefile可以做如下改进

1
2
3
4
block.o command.o input.o scene.o test.o: %.o : %.cpp %.h
g++ -c $<
main.o: main.cpp scene.h input.h test.h
g++ -c main.cpp

用这种方式可以在简写的同时一定程度上解决各个目标对应的依赖问题。

(不属于静态模式的内容,隐式规则的内容)利用模式匹配可以直接将所有.cpp到.o文件的编译简写为如下

1
2
%.o : %.cpp %.h
g++ -c $<

条件判断

使用条件指令可以让make执行或略过Makefile文件中的一些部分。

ifdef 判断一个变量是已否定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OS = Linux
ifdef Win
OS = Windows
endif


OS = Linux
ifdef Win
OS = Windows
else ifdef Mac
OS= MacOS
endif


ifdef Win
OS = Windows
else ifdef Mac
OS= MacOS
else
OS = Linux
endif

ifndef 判断一个变量是否没被定义

1
2
3
ifndef FLAGS
FLAGS = -finput-charset=utf-8
endif

ifeq 判断两个值是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version = 3.0

ifeq ($(version),1.0) # ifeq后一定要一个空格
msg := 版本太旧了,请更新版本
else ifeq ($(version), 3.0)
msg := 版本太新了,也不行
else
msg := 版本可以用
endif


# 另外的写法
msg = Other
ifeq "$(OS)" "Windows_NT"
msg = This is a Windows Platform
endif

ifeq '$(OS)' 'Windows_NT'

ifeq '$(OS)' "Windows_NT"

ifneq 判断两个值是否不等

用法及参数同ifeq,只是判断结果相反

 

 

 

 

 

文本处理函数

C语言中,函数调用方法是function(arguments);但在Makefile中调用函数的写法不同

1
2
$(function arguments) 或 ${function arguments}
$(function arg1,$(arg2),arg3 ...) # 参数之间不要有空格

字符替换与分析

subst

文本替换函数,返回替换后的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$(subst target,replacement,text)
--- 用relacement替换text中的target
--- target 需要替换的内容
--- replacement 替换为的内容
--- text 需要处理的内容,可以是任意字符串



objs = main.o hello.o
srcs = $(subst .o,.cpp,$(objs))
headers = $(subst .cpp,.h,$(srcs))

all:
@echo $(srcs)
@echo $(headers)

patsubst

模式替换, 返回替换后的文本

1
2
3
4
5
6
7
8
9
$(patsubst pattern,replacement,text)
--- pattern 需要替换的模式
--- replacement 需要替换为
--- text 待处理内容,各项内容需要用空格隔开


objs = main.ohello.o
srcs = $(subst %.o,%.cpp,$(objs))
headers = $(subst %.cpp,%.h,$(srcs))

strip

去除字符串头部和尾部的空格,中间如果连续有多个空格,则用一个空格替换,返回去除空格后的文本

1
2
3
4
5
6
7
$(strip string)
--- string 需要去除空格的字符串


files = aa hello.cpp main.cpp test.cpp
files := $(subst aa, ,$(files))
files2 = $(strip $(files))

findstring

查找字符串,如果找到了,则返回对应的字符串,如果没找到,则反回空串

1
2
3
4
5
6
7
$(findstring find,string)
--- find 需要查找的字符串
--- string 用来查找的内容

files = hello.cpp main.cpp test.cpp
find = $(findstring hel,$(files))
find = $(findstring HEL,$(files))

filter

从文本中筛选出符合模式的内容并返回

1
2
3
4
5
6
$(filter pattern…,text)
--- pattern 模式,可以有多个,用空格隔开
--- text 用来筛选的文本,多项内容需要用空格隔开,否则只会当一项来处理

files = hello.cpp main.cpp test.cpp main.o hello.o hello.h
files2 = $(filter %.o %.h,$(files))

filter-out

与filter相反,过滤掉符合模式的,返回剩下的内容

1
2
3
4
5
6
7
$(filter-out pattern…,text)
--- pattern 模式,可以有多个,用空格隔开
--- text 用来筛选的文本,多项内容需要用空格隔开,否则只会当一项来处理


files = hello.cpp main.cpp test.cpp main.o hello.o hello.h
files2 = $(filter-out %.o %.cpp,$(files))

sort

将文本内的各项按字典顺序排列,并且移除重复项

1
2
3
4
5
6
$(sort list)
--- list 需要排序内容


files = hello.cpp main.cpp test.cpp main.o hello.o hello.h main.cpp hello.cpp
files2 = $(sort $(files))

word

用于返回文本中第n个单词

1
2
3
4
5
6
$(word n,text)
--- n 第n个单词,从1开始,如果n大于总单词数,则返回空串
--- text 待处理文本

files = hello.cpp main.cpp test.cpp main.o hello.o hello.h main.cpp hello.cpp
files2 = $(word 3,$(files))

wordlist

用于返回文本指定范围内的单词列表

1
2
3
4
5
6
$(wordlist start,end,text)
--- start 起始位置,如果大于单词总数,则返回空串
--- end 结束位置,如果大于单词总数,则返回起始位置之后全部,如果start > end,什么都不返回

files = hello.cpp main.cpp test.cpp main.o hello.o hello.h main.cpp hello.cpp
files2 = $(wordlist 3,6,$(files))

words

返回文本中单词数

1
2
3
4
5
6
$(words text)
--- text 需要处理的文本


files = hello.cpp main.cpp test.cpp main.o hello.o hello.h main.cpp hello.cpp
nums = $(words $(files))

firstword

返回第一个单词

1
$(firstword text)

lastword

返回最后一个单词

1
$(lastword text)

文件名处理函数

dir

返回文件目录

1
2
3
4
5
6
$(dir files)
--- files 需要返回目录的文件名,可以有多个,用空格隔开

files = src/hello.cpp main.cpp

files2 = $(dir $(files))

notdir

返回除目录部分的文件名

1
2
3
4
5
$(notdir files)
--- files 需要返回文件列表,可以有多个,用空格隔开

files = src/hello.cpp main.cpp
files2 = $(notdir $(files))

suffix

返回文件后缀名,如果没有后缀返回空

1
2
3
4
5
6
$(suffix files)
--- files 需要返回后缀的文件名,可以有多个,用空格隔开


files = src/hello.cpp main.cpp hello.o hello.hpp hello
files2 = $(suffix $(files))

basename

返回文件名除后缀的部分

1
2
3
4
5
6
$(basename files)
--- files 需要返回的文件名,可以有多个,用空格隔开


files = src/hello.cpp main.cpp hello.o hello.hpp hello
files2 = $(basename $(files))

addsuffix

给文件名添加后缀

1
2
3
4
5
6
$(addsuffix suffix,files)
--- suffix 需要添加的后缀
--- files 需要添加后缀的文件名,可以有多个,用空格隔开

files = src/hello.cpp main.cpp hello.o hello.hpp hello
files2 = $(addsuffix .exe,$(files))

addprefix

给文件名添加前缀

1
2
3
4
5
6
$(addprefix prefix,files)
--- prefix 需要添加的前缀
--- files 需要添加前缀的文件名,可以有多个,用空格隔开

files = src/hello.cpp main.cpp hello.o hello.hpp hello
files2 = $(addprefix make/,$(files))

join

将两个列表中的内容一对一连接,如果两个列表内容数量不相等,则多出来的部分原样返回

1
2
3
4
5
6
7
8
$(join list1,list2)
--- list1 第一个列表
--- list2 需要连接的第二个列表


f1 = hello main test
f2 = .cpp .hpp
files2 = $(join $(f1),$(f2))

wildcard

返回符合通配符的文件列表

1
2
3
4
5
6
$(wildcard pattern)
--- pattern 通配符

files2 = $(wildcard *.cpp)
files2 = $(wildcard *)
files2 = $(wildcard src/*.cpp)

realpath

返回文件的绝对路径

1
2
3
4
5
$(realpath files)
--- files 需要返回绝对路径的文件,可以有多个,用空格隔开

f3 = $(wildcard src/*)
files2 = $(realpath $(f3))

abspath

返回绝对路径,用法同realpath,如果一个文件名不存在,realpath不会返回内容,abspath则会返回一个当前文件夹一下的绝对路径

1
$(abspath files)

条件函数

if

条伯判断,如果条件展开不是空串,则反回真的部分,否则返回假的部分

1
2
3
4
5
6
7
$(if condition,then-part[,else-part])
--- condition 条件部分
--- then-part 条件为真时执行的部分
--- else-part 条件为假时执行的部分,如果省略则为假时返回空串

files = src/hello.cpp main.cpp hello.o hello.hpp hello
files2 = $(if $(files),有文件,没有文件)

or

返回条件中第一个不为空的部分

1
2
3
4
5
6
7
$(or condition1[,condition2[,condition3…]])

f1 =
f2 =
f3 = hello.cpp
f4 = main.cpp
files2 = $(or $(f1),$(f2),$(f3),$(f4))

and

如果条件中有一个为空串,则返回空,如果全都不为空,则返回最后一个条件

1
2
3
4
5
6
7
$(and condition1[,condition2[,condition3…]])

f1 = 12
f2 = 34
f3 = hello.cpp
f4 = main.cpp
files2 = $(and $(f1),$(f2),$(f3),$(f4))

intcmp

比较两个整数大小,并返回对应操作结果(GNU make 4.4以上版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]]) 
--- lhs 第一个数
--- rhs 第二个数
--- lt-part lhs < rhs时执行
--- eq-part lhs = rhs时执行
--- gt-part lhs > rhs时执行
--- 如果只提供前两个参数,则lhs == rhs时返回数值,否则返回空串
参数为lhs,rhs,lt-part时,当lhs < rhs时返回lt-part结果,否则返回空
参数为lhs,rhs,lt-part,eq-part,lhs < rhs返回lt-part结果,否则都返回eq-part结果
参数全时,lhs < rhs返回lt-part,lhs == rhs返回eq-part, lhs > rhs返回gt-part



@echo $(intcmp 2,2,-1,0,1)

file

读写文件

1
2
3
4
5
6
7
8
9
10
11
12
$(file op filename[,text])
--- op 操作
> 覆盖
>> 追加
< 读
--- filename 需要操作的文件名
--- text 写入的文本内容,读取是不需要这个参数


files = src/hello.cpp main.cpp hello.o hello.hpp hello
write = $(file > makewrite.txt,$(files))
read = $(file < makewrite.txt)

foreach

对一列用空格隔开的字符序列中每一项进行处理,并返回处理后的列表

1
2
3
4
5
6
7
$(foreach each,list,process)
--- each list中的每一项
--- list 需要处理的字符串序列,用空格隔开
--- process 需要对每一项进行的处理

list = 1 2 3 4 5
result = $(foreach each,$(list),$(addprefix cpp,$(addsuffix .cpp,$(each))))

作用类似C/C++中的循环

1
2
3
4
5
6
7
8
9
int list[5] = {1, 2, 3, 4, 5};
int result[5];
int each;
for(int i = 0; i < 5; i++)
{
each = list[i];
result[i] = process(each);
}
// 此时result即为返回结果

call

将一些复杂的表达式写成一个变量,用call可以像调用函数一样进行调用。类似于编程语言中的自定义函数。在函数中可以用$(n)来访问第n个参数

1
2
3
4
5
6
$(call funcname,param1,param2,…)
--- funcname 自定义函数(变量名)
--- 参数至少一个,可以有多个,用逗号隔开

dirof = $(dir $(realpath $(1))) $(dir $(realpath $(2)))
result = $(call dirof,main.cpp,src/hello.cpp)

value

对于不是立即展开的变量,可以查看变量的原始定义;对于立即展开的变量,直接返回变量值

1
2
3
4
5
6
7
8
$(value variable)

var = value function test
var2 = $(var)
var3 := $(var)
all:
@echo $(value var2)
@echo $(value var3)

查看一个变量定义来源

1
2
3
4
5
6
7
8
9
10
$(origin variable)


var2 = origin function
all:
@echo $(origin var1) # undefined 未定义
@echo $(origin CC) # default 默认变量
@echo $(origin JAVA_HOME) # environment 环境变量
@echo $(origin var2) # file 在Makefile文件中定义的变量
@echo $(origin @) # automatic 自动变量

flavor

查看一个变量的赋值方式

1
2
3
4
5
6
7
8
$(flavor variable)

var2 = flavor function
var3 := flavor funciton
all:
@echo $(flavor var1) # undefined 未定义
@echo $(flavor var2) # recursive 递归展开赋值
@echo $(flavor var3) # simple 简单赋值

eval

可以将一段文本生成Makefile的内容

1
2
3
4
5
6
7
8
$(eval text)

define eval_target =
eval:
@echo Target Eval Test
endef

$(eval $(eval_target))

以上,运行make时将会执行eval目标

shell

用于执行Shell命令

1
2
files = $(shell ls *.cpp)
$(shell echo This is from shell function)

let

将一个字符串序列中的项拆开放入多个变量中,并对各个变量进行操作(GNU make 4.4以上版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$(let var1 [var2 ...],[list],proc)
--- var 变量,可以有多个,用空格隔开
--- list 待处理字符串,各项之间空格隔开
--- proc 对变量进行的操作,结果为let的返回值
将list中的值依次一项一项放到var中,如果var的个数多于list项数,那多出来的var是空串。如果
var的个数小于list项数,则先依次把前而的项放入var中,剩下的list所有项都放入最后一个var中


list = a b c d
letfirst = $(let first second rest,$(list),$(first))
letrest = $(let first second rest,$(list),$(rest))


# 结合call可以对所有项进行递归处理
reverse = $(let first rest,$(1),$(if $(rest),$(call reverse,$(rest)) )$(first))
all: ; @echo $(call reverse,d c b a)

信息提示函数

error

提示错误信息并终止make执行

1
2
3
4
5
6
7
$(error text)
--- text 提示信息

EXIT_STATUS = -1
ifneq (0, $(EXIT_STATUS))
$(error An error occured! make stopped!)
endif

warning

提示警告信息,make不会终止

1
2
3
4
5
$(warning text)

ifneq (0, $(EXIT_STATUS))
$(warning This is a warning message)
endif

info

输出一些信息

1
2
3
4
$(info text…)

$(info 编译开始.......)
$(info 编译结束)

同一项目中有多个Makefile文件

包含其他makefile文件

使用include指令可以读入其他makefile文件的内容,效果就如同在include的位置用对应的文件内容替换一样。

1
2
include mkf1 mkf2 # 可以引入多个文件,用空格隔开
include *.mk # 可以用通配符,表示引入所有以.mk结尾的文件

如果找不到对应文件,则会报错,如果要忽略错误,可以在include前加-

1
-include mkf1 mkf2

应用实例:自动生成依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
objs = block.o command.o input.o main.o scene.o test.o

sudoku: $(objs)
g++ $(objs) -o sudoku

include $(objs:%.o=%.d)

%.d: %.cpp
@-rm $@
$(CXX) -MM $< > $@.temp
@sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.temp > $@
@-rm $@.temp


%.o : %.cpp
g++ -c $<
@echo $^

嵌套make

如果将一个大项目分为许多小项目,则可以使用嵌套(递归)使用make。具体做法为,写一个总的Makefile,然后在每个子项目中都写一个Makefile,在总Makefile中进行调用。

例如,可以把sudoku项目中除main.cpp,test.cpp外的其他cpp存为一个子项目,编译为一个库文件,main.cpp test.cpp为另一个子项目,编译为.o然后链接库文件成可执行文件:

库文件Makefile

1
2
3
4
5
6
7
8
9
10
11
vpath %.h ../include

CXXFLAGS += -I../include -fexec-charset=GBK -finput-charset=UTF-8

cpps := $(wildcard *.cpp)
objs := $(cpps:%.cpp=%.o)

libsudoku.a: $(objs)
ar rcs $@ $^

$(objs): %.o : %.cpp %.h

main.cpp test.cpp的Makefile

1
2
3
4
5
6
CXXFLAGS += -I../include -fexec-charset=GBK -finput-charset=UTF-8
vpath %.h ../include
vpath %.a ../lib

../sudoku: main.o test.o -lsudoku
$(CXX) -o $@ $^

总的Makefile

1
2
3
4
5
6
7
8
9
10
11
12
.PHONY: all clean

all: subsrc

subsrc: sublib
$(MAKE) -C src

sublib:
$(MAKE) -C lib

clean:
-rm *.exe src/*.o lib/*.o lib/*.a

其中

1
$(MAKE) -C subdir

这一指令会自动进入subdir文件夹然后执行make。

可以通过export指令向子项目的make传递变量。

1
2
3
export var  # 传递var
export # 传递所有变量
unexport # 取消传递

 

 

 

 

后续学习过程

读一些开源项目的Makefile

redis:https://github.com/redis/redis

ffmpeg:https://github.com/FFmpeg/FFmpeg

aubio:https://github.com/aubio/aubio

libav:https://github.com/libav/libav

OpenH264:https://github.com/cisco/openh264

TinyVM:https://github.com/jakogut/tinyvm

TinyXML2:https://github.com/leethomason/tinyxml2

一个 Makefile 的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

要产生 可执行文件 edit, 输入 make.
要删除可执行文件,临时文件, 输入 make clean.

目标如果没有明确指明文件,而只是动作, 称作 phony 目标。

通常make 从第一个目标开始编译,也就是为什么我们不需要 输入 make edit, 而只要 make 即可.

上面的 .o 文件被重复多次, 容易产生错误。 使用 变量 variable 可解决此问题。

我们可以定义一个变量, 他包含一个字符串
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

然后可以用此变量代替, 产生如下 makefile

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)

make 可以 从 .c 文件推断需要产生一个 .o 文件。
.c 文件还可以自动被添加到必要文件 栏中。
例如如下 makefile.

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)

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
clean :
rm edit $(objects)

上面的makefile 还可以写成如下形式:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
rm edit $(objects)

上面行 .PHONY : clean 告诉 编译器 不要将 文件clean与我们这里的动作 clean 搞混淆了。

使用 make -f 或者 make --file 来通知编译器要使用的 makefile 名字。

include filename ... 来让 make把以下文件包括进来。

一、函数的调用语法

函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:

$(<function> <arguments> )

或是

${<function> <arguments>}

这里,<function>就是函数名,make支持的函数不多。<arguments>是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。
函数调用以“$”开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?
函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用“$(subst a,b,$(x))”这样的形式,
而不是“$(subst a,b,${x})”的形式。因为统一会更清楚,也会减少一些不必要的麻烦。

还是来看一个示例:

comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

在这个示例中,$(comma)的值是一个逗号。$(space)使用了$(empty)定义了一个空格,$(foo)的值是“a b c”,$(bar)的定义用,
调用了函数“subst”,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。
这个函数也就是把$(foo)中的空格替换成逗号,所以$(bar)的值是“a,b,c”。

二、字符串处理函数

$(subst <from>,<to>,<text> )

名称:字符串替换函数——subst。
功能:把字串<text>中的<from>字符串替换成<to>。
返回:函数返回被替换过后的字符串。

示例:

$(subst ee,EE,feet on the street)

把“feet on the street”中的“ee”替换成“EE”,返回结果是“fEEt on the strEEt”。

$(patsubst <pattern>,<replacement>,<text> )

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

示例:

$(patsubst %.c,%.o,x.c.c bar.c)

把字串“x.c.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.c.o bar.o”

备注:

这和我们前面“变量章节”说过的相关知识有点相似。如:

“$(var:<pattern>=<replacement> )”
相当于
$(patsubst <pattern>,<replacement>,$(var))”,

而“$(var: <suffix>=<replacement> )”
则相当于
$(patsubst %<suffix>,%<replacement>,$(var))”。

例如有:objects = foo.o bar.o baz.o,
那么,“$(objects:.o=.c)”和“$(patsubst %.o,%.c,$(objects))”是一样的。

$(strip <string> )

名称:去空格函数——strip。
功能:去掉<string>字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
示例:

$(strip a b c )

把字串“a b c ”去到开头和结尾的空格,结果是“a b c”。

$(findstring <find>,<in> )

名称:查找字符串函数——findstring。
功能:在字串<in>中查找<find>字串。
返回:如果找到,那么返回<find>,否则返回空字符串。
示例:

$(findstring a,a b c)
$(findstring a,b c)

第一个函数返回“a”字符串,第二个返回“”字符串(空字符串)

$(filter <pattern...>,<text> )

名称:过滤函数——filter。
功能:以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词。可以有多个模式。
返回:返回符合模式<pattern>的字串。
示例:

sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo

$(filter %.c %.s,$(sources))返回的值是“foo.c bar.c baz.s”。

$(filter-out <pattern...>,<text> )

名称:反过滤函数——filter-out。
功能:以<pattern>模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词。可以有多个模式。
返回:返回不符合模式<pattern>的字串。
示例:

objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o

$(filter-out $(mains),$(objects)) 返回值是“foo.o bar.o”。

$(sort <list> )

名称:排序函数——sort。
功能:给字符串<list>中的单词排序(升序)。
返回:返回排序后的字符串。
示例:$(sort foo bar lose)返回“bar foo lose” 。
备注:sort函数会去掉<list>中相同的单词。

$(word <n>,<text> )

名称:取单词函数——word。
功能:取字符串<text>中第<n>个单词。(从一开始)
返回:返回字符串<text>中第<n>个单词。如果<n>比<text>中的单词数要大,那么返回空字符串。
示例:$(word 2, foo bar baz)返回值是“bar”。

$(wordlist <s>,<e>,<text> )

名称:取单词串函数——wordlist。
功能:从字符串<text>中取从<s>开始到<e>的单词串。<s>和<e>是一个数字。
返回:返回字符串<text>中从<s>到<e>的单词字串。如果<s>比<text>中的单词数要大,那么返回空字符串。如果<e>大于<text>的单词数,那么返回从<s>开始,到<text>结束的单词串。
示例: $(wordlist 2, 3, foo bar baz)返回值是“bar baz”。

$(words <text> )

名称:单词个数统计函数——words。
功能:统计<text>中字符串中的单词个数。
返回:返回<text>中的单词数。
示例:$(words, foo bar baz)返回值是“3”。
备注:如果我们要取<text>中最后的一个单词,我们可以这样:$(word $(words <text> ),<text> )。

$(firstword <text> )

名称:首单词函数——firstword。
功能:取字符串<text>中的第一个单词。
返回:返回字符串<text>的第一个单词。
示例:$(firstword foo bar)返回值是“foo”。
备注:这个函数可以用word函数来实现:$(word 1,<text> )

以上,是所有的字符串操作函数,如果搭配混合使用,可以完成比较复杂的功能。这里,举一个现实中应用的例子。我们知道,make使用“VPATH”变量来指定“依赖文件”的搜索路径。于是,我们可以利用这个搜索路径来指定编译器对头文件的搜索路径参数CFLAGS,如:

override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

如果我们的“$(VPATH)”值是“src:../headers”,那么“$(patsubst %,-I%,$(subst :, ,$(VPATH)))”将返回“-Isrc -I../headers”,这正是cc或gcc搜索头文件路径的参数。


三、文件名操作函数

下面我们要介绍的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是一系列的文件名来对待。

$(dir <names...> )

名称:取目录函数——dir。
功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
返回:返回文件名序列<names>的目录部分。
示例: $(dir src/foo.c hacks)返回值是“src/ ./”。

$(notdir <names...> )

名称:取文件函数——notdir。
功能:从文件名序列<names>中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后的部分。
返回:返回文件名序列<names>的非目录部分。
示例: $(notdir src/foo.c hacks)返回值是“foo.c hacks”。

$(suffix <names...> )

名称:取后缀函数——suffix。
功能:从文件名序列<names>中取出各个文件名的后缀。
返回:返回文件名序列<names>的后缀序列,如果文件没有后缀,则返回空字串。
示例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。

$(basename <names...> )

名称:取前缀函数——basename。
功能:从文件名序列<names>中取出各个文件名的前缀部分。
返回:返回文件名序列<names>的前缀序列,如果文件没有前缀,则返回空字串。
示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是“src/foo src-1.0/bar hacks”。

$(addsuffix <suffix>,<names...> )

名称:加后缀函数——addsuffix。
功能:把后缀<suffix>加到<names>中的每个单词后面。
返回:返回加过后缀的文件名序列。
示例:$(addsuffix .c,foo bar)返回值是“foo.c bar.c”。

$(addprefix <prefix>,<names...> )

名称:加前缀函数——addprefix。
功能:把前缀<prefix>加到<names>中的每个单词后面。
返回:返回加过前缀的文件名序列。
示例:$(addprefix src/,foo bar)返回值是“src/foo src/bar”。

$(join <list1>,<list2> )

名称:连接函数——join。
功能:把<list2>中的单词对应地加到<list1>的单词后面。如果<list1>的单词个数要比<list2>的多,那么,<list1>中的多出来的单词将保持原样。如果<list2>的单词个数要比<list1>多,那么,<list2>多出来的单词将被复制到<list2>中。
返回:返回连接过后的字符串。
示例:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。

四、foreach 函数


foreach函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的。它的语法是:



$(foreach <var>,<list>,<text> )



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



所以,<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会使用<var>这个参数来依次枚举<list>中的单词。举个例子:



names := a b c d

files := $(foreach n,$(names),$(n).o)



上面的例子中,$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)的值是“a.o b.o c.o d.o”。



八、shell函数


shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:



contents := $(shell cat foo)



files := $(shell echo *.c)



注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。







注意,foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不在作用,其作用域只在foreach函数当中。

Makefile学习总结
https://ysc2.github.io/ysc2.github.io/2024/04/16/Makefile学习总结/
作者
Ysc
发布于
2024年4月16日
许可协议