零、概述

Makefile是一种用于构建和管GNU软件项目的自动化构建工具make的配置文件,定义了目标、依赖构建规则的概念。

  • 目标就是要做的事、要产出的内容、可执行文件等。
  • 依赖就是产出目标的原料、源文件、条件等等。
  • 构建规则就是用依赖产出目标的过程或是指令。

很多IDE软件中的编译按键,实际上也是调用的Makefile作为配置文件。一般在Linux中编译文件使用的是,在命令行中输入 make 命令来编译工程,那么这期间做了什么事呢?

  • make 命令会在当前目录下查找是否存在“Makefile”这个文件,也可以make的时候指定make -f your.mk
  • 存在则按照Makefile 中定义的编译方式进行编译
  • 文件改动后make,会对改动的自动编译,会对比目标和依赖的时间,依赖时间超前于目标的部分,就会重新进行编译。

一、Makefile特性

1 格式

Makefile遵循一定的格式,一般如下:

1
2
目标文件:依赖文件
[Tab键]指令

这边的键是属于格式的一部分,不可以使用空格代替。

其中指令部分也就是编译过程中要执行的命令,常见的就是gcc xxx

1
2
3
.PHONY:all
all:
echo "start compiling..."

运行如下:

image-20241229211138180

可以看到不经将指令结果运行出来了,还将该条指令打印了出来,如果不想打印指令本身,可以在前面添加一个@来屏蔽指令本身,如下:

image-20241229211321985

这样就只会打结果了。

2 特性

2.1 变量

1
2
3
4
5
A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一次定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果 D 在前面是延时变量,那么现在它还是延时变量;
// 如果 D 在前面是立即变量,那么现在它还是立即变量

假设定义了一个变量val,可以通过以下方式引用:

  • $(val)
  • ${val}

在shell中是通过$( )来引用变量的。

2.1.1 立即变量

立即变量的意思就是在解析赋值语句的时候就将变量值赋值到对应的变量上。

1
2
3
4
5
6
.PHONY: all
old_a = 1
A := $(old_a)
old_a = 2
all:
@echo "A = $(A)"

运行如下:

image-20241229212242137

可以看到old_a的值立即赋值到A上,所以在后续old_a改变了,A的是还是之前赋值的。

2.1.2 延时变量

延时赋值,可以理解成用到再赋值,在解析语句的时候没有立即赋值,而是在真正使用到的时候才将值赋值进去,有点全局的意思。

1
2
3
4
5
6
.PHONY: all
old_a = 1
A = $(old_a)
old_a = 2
all:
@echo "A = $(A)"

还是上一个例子,只是将立即赋值改成延时赋值,运行如下:

image-20241229212612093

可以看到这边的值,是在使用的才赋值上去,而在此之前被新的值覆盖了。更有说服力的例子如下:

1
2
3
4
.PHONY: all
A = $@
all:
@echo $A

上述 Makefile 中,变量 A 的值在执行时才确定,它等于 test,是延时变量。如果使用“ A := $@”,这是立即变量,这时$@为空,所以 A 的值就是空。

运行如下:

image-20241229212747623

2.1.3 追加变量

追加变量常见于编译参数的追加,如下:

1
2
3
4
5
.PHONY:all
CFLAGS = -g
CFLAGS += -Wall
all:
@echo "CFLAGS = @(CFLAGS)"

运行如下:

image-20241229213128040

可以看到CFLAGS最终是追加后的结果。

2.1.4 变量替换

将一个变量中的内容进行替换

1
2
3
4
5
6
.PHONY: all
SRC := main.c sub.c
OBJ := $(SRC:.c=.o)
all:
@echo "SRC = $(SRC)"
@echo "OBJ = $(OBJ)"

运行如下:

image-20241229210833919

2.1.5 环境变量

在make的时候会将一些环境变量引入到Makefile中,例如CFLAGS、SHELL、MAKE等变量。

1
2
3
4
5
6
7
.PHONY:all
CFLAGS = -g
all:
@echo "CFLAGS = $(CFLAGS)"
@echo "SHELL = $(SHELL)"
@echo "MAKE = $(MAKE)"
@echo "HOSTNAME = $(HOSTNAME)"

运行如下:

image-20241229202137350

这个是原先就有的环境变量,当然也可以通过export来定义的临时环境变量。

1
2
3
.PHONY:all
all:
@echo "new para = $(para)"

运行如下:

image-20241229214231597

2.1.6 递归传递变量

在使用递归传递变量前,需要了解的是,递归运行Makefile,也就是可以在顶层目录中调用下一个目录的Makefile,结构如下:

1
2
3
4
.
├── Makefile
└── src
└── Makefile

内容如下:

image-20241229214710513

运行如下:

image-20241229214738589

可以看到在./中的Makefile中调用了./src/下的Makefile文件。

现在来看变量的递归使用

就是在顶层Makefile中定义一个变量,然后在下一级Makefile中调用该变量,运行如下:

image-20241229215040736

可以看到,在./src/Makefile中正确获取到顶层Makefile中定义的变量para

2.1.4 override变量

现在假设有一个变量你不希望被其他人轻易改变,或者说被无意修改,就可以给该变量加上override前缀,修改上个例子中的src下的Makefile:

1
2
3
4
.PHONY:all
override para=hello
all:
@echo "new para = $(para)"

运行如下:

image-20241229215557838

可以看到,para变量可能中上一层Makefile递归下来,但是有些时候不希望被修改就可以这么用。

那如果一定想把override修饰的变量进行修改的话,就需要也加上override,但是不建议这么干,除非你清楚地知道自己在干嘛:

1
2
3
4
5
.PHONY:all
override para=hello
override para=world
all:
@echo "new para = $(para)"

运行如下:

image-20241229215930332

可以看到确实是被新的赋值给覆盖了。

2.1.5 通配符(自动变量)

1
2
3
4
5
6
7
8
9
10
1.通配符 pattan
test : main.o sub.o
gcc -o test main.o sub.o (main.o sub.o可以用$^代替)

test : main.o sub.o
gcc -o test $^ (main.o sub.o可以用$^代替,表示所有依赖文件)
%.o : %.c
gcc -c -o $@ $< ($@表示目标 即%.o $<表示第一个依赖文件 即%.c)
clean:
rm *.o test -f

2.1.6 后缀替换

变量是文件列表的时候用于后缀替换

1
2
3
4
5
6
7
.PHONY:all

val = main.c hello.c
val_new = $(val:.c=.o)

all:
@echo "new para = $(val_new)"

运行如下:

image-20241229221521205

2.2 条件判断

有时候需要根据不同的宏来做不同的编译动作,例如当前编译的是调试版本还是发布版本,格式如下:

1
2
3
4
5
ifeq ($(xxx),yyy) #取到xxx变量的值,对比是不是等于yyy

else

endif

举例如下:

1
2
3
4
5
6
7
8
.PHONY:all
mode = debug
all:
ifeq ($(mode),debug)
@echo "debug mode"
else
@echo "release mode"
endif

运行如下:

image-20241229220329690

二、常用函数

1 文本处理

用于处理文本的函数,例如后缀替换、空格去除、内容过滤等等。

1.1 subst函数

可以将变量中的内容替换成其他的,例如将变量中的.c替换成.o,如下:

1
2
3
4
5
6
7
.PHONY:all

SRC = main.c hello.c
OBJ = $(subst .c,.o,$(SRC))

all:
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105205348963

这个是用于替换文本,不一定得是后缀,也可以是其他得,例如将o换成6:

image-20250105205732955

1.2 patsubst函数

这个函数和上述得$(subst )区别是:以空格为分隔符,可以利用通配符来完成替换,符合条件即可替换,如下:

1
2
3
4
5
6
7
8
.PHONY:all

SRC = main.c hello.c

OBJ = $(patsubst %.c,%.o,$(SRC))

all:
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105205955992

1.3 strip函数

strip函数可以将多余的空格进行剔除,确保源文本中内容之间的空格只有一个,如:在main.chello.c之间加入大量的空格。

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.c

OBJ = $(strip $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105210517148

1.4 findstring函数

findstring函数可以在源文本中找到符合条件的文本信息(字符串),如下:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.c

OBJ = $(findstring main.c,$(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105210803349

1.5 filter 函数

filter函数可以用来过滤出来符合条件的文本,可以是单纯的文本也可以是通配符例如:

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

SRC = main.c hello.h hello.c sub.c add.h

OBJ = $(filter main.c, $(SRC))
OBJ1 = $(filter %.c, $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"
@echo "new OBJ1 = $(OBJ1)"

运行如下:

image-20250105211328696

1.6 filter-out 函数

filter-out函数和fliter函数类中,不同的是用来过滤剔除符合条件的文本可以是单纯的文本也可以是通配符例如:

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

SRC = main.c hello.h hello.c sub.c add.h

OBJ = $(filter-out main.c, $(SRC))
OBJ1 = $(filter-out %.c, $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"
@echo "new OBJ1 = $(OBJ1)"

运行如下:

image-20250105211545703

可以看到,符合条件的文本被剔除了,只剩下不符合条件的文本信息。

1.7 sort函数:单词排序

sort函数会将源文本中的每一个文件按文件名首字母进行排序,删除重复的单词,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.h hello.c sub.c add.h back.c

OBJ = $(sort $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105212051659

加入重复文件名后:

image-20250105212249458

1.8 word函数:取单词

word函数可以取源文本中的第n个单词信息,例如:

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

SRC = main.c hello.h hello.c sub.c add.h back.c main.c

OBJ = $(word 1,$(SRC))
OBJ1 = $(word 2,$(SRC))
OBJ2 = $(word 3,$(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"
@echo "new OBJ = $(OBJ1)"
@echo "new OBJ = $(OBJ2)"

运行如下:

image-20250105212423326

如图所示,分别取第1,2,3个单词。

1.9 wordlist函数:取字串

wordlist函数用来从一个源文本中取出从n到m之间的一个单词串,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.h hello.c sub.c add.h back.c main.c

OBJ = $(wordlist 1,3,$(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105212653337

如图取出第1到第3的单词。

1.10 words函数:统计单词数目

words函数可以统计源文本中包含多少个单词,单词之间是通过空格分隔的,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.h hello.c sub.c add.h back.c main.c

OBJ = $(words $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105212832406

1.11 firstword函数:取首个单词

可以取到整个源文本中最开头的单词,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.h hello.c sub.c add.h back.c main.c

OBJ = $(firstword $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105212945665

2 文件处理

用于处理文件相关操作,文件名替换、前缀后缀处理、目录处理等。

2.1 dir函数:取路径名的目录

dir函数用来从一个路径名中截取目录的部分,例如:

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

SRC = /home/czr/workspaces/czr_test/make_stu/src/Makefile
SRC1 = /home/czr/workspaces/czr_test/make_stu/src

OBJ = $(dir $(SRC))
OBJ1 = $(dir $(SRC1))

all:
@echo "new SRC = $(SRC)"
@echo "new SRC1 = $(SRC1)"
@echo "new OBJ = $(OBJ)"
@echo "new OBJ1 = $(OBJ1)"

运行如下:

image-20250105213359731

如果路径是目录的就会取前一级目录路径,因为文本而言并不知道这个是不是目录,如果最后是/结尾的,那么结果将是空。

2.2 notdir函数:取文件名

notdir函数顾名思义,就是提出目录部分只取文件名,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = /home/czr/workspaces/czr_test/make_stu/src/Makefile

OBJ = $(notdir $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105214007785

2.3 suffix函数:取文件名后缀

suffix函数可以取文件名后缀,假设有main.cpp就会取到.cpp,例如

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.cpp

OBJ = $(suffix $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105214221502

2.4 basename函数:取文件名前缀

basename函数可以取文件名前缀,假设有main.cpp就会取到main,例如

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.cpp

OBJ = $(basename $(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105214346219

2.5 addsuffix函数:给文件名加后缀

addsuffix函数可以为文本变量添加后缀,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main hello

OBJ = $(addsuffix .cpp,$(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105214508586

2.6 addprefix函数:给文件名加前缀

addprefix函数可以给文件名加指定的前缀信息,例如:

1
2
3
4
5
6
7
8
9
.PHONY:all

SRC = main.c hello.c

OBJ = $(addprefix tmp.,$(SRC))

all:
@echo "new SRC = $(SRC)"
@echo "new OBJ = $(OBJ)"

运行如下:

image-20250105214604795

2.7 wildcard函数:列出所有符号匹配模式的文件(很常用)

这个函数非常经常用,可以按格式列出符合条件的文件名,例如:

1
2
3
4
5
6
.PHONY:all

SRC = $(wildcard *.c)

all:
@echo "new SRC = $(SRC)"

运行如下:

image-20250105215058872

Linux中一般都会先通过wildcard函数获取到需要编译的源文件,在进行处理,例如替换后缀为需要的目标文件等等的操作。

3 其他函数

3.1 if函数

Makefile中的 if 函数提供了在一个函数上下文中实现条件判断的功能,类似于ifeq关键字,if函数的使用格式如下:

1
2
3
4
5
6
$(if CONDITION,THEN-PART)
$(if CONDITION,THEN-PART[,ELSE-PART])

CONDITION:条件
THEN-PART:符合条件后执行
ELSE-PART:不符合条件后执行

举例如下:

1
2
3
4
5
6
7
8
.PHONY:all

dir = /usr/bin

real_dir = $(if $(dir),$(dir),/usr/bin/new)

all:
@echo "real_dir = $(real_dir)"

运行如下:

image-20250105220516605

可以看到当dir有定义的时候,就输出dir变量的值,当dir没有定义的时候就会使用后面的值。

3.2 call函数

后面自定义函数中使用,格式如下:

1
$(call <expression>,<parm1>,<parm2>,<parm3>...)

parm1等变量会被带入到函数中,在函数中体现为对应的$(1)等。

3.3 origin函数

顾名思义,origin函数的作用就是告诉你,你所关注的一个变量是从哪里来的。函数的使用格式为:

1
$(origin <variable>)

主要有以下选项:

  • default:变量是一个默认的定义,比如 CC 这个变量
  • file:这个变量被定义在Makefile
  • command line:这个变量是被命令行定义的
  • override:这个变量是被override指示符重新定义过的
  • automatic:一个命令运行中的自动化变量
  • undefined:未定义

举例:

1
2
3
4
5
6
7
8
.PHONY: all

type1 = hello

all:
@echo "type1 = $(origin type1)"
@echo "type2 = $(origin CC)"
@echo "type3 = $(origin para)"

运行如下:

image-20250105221201483

3.4 shell 函数

可以在makefile中运行shell命令,然后将获取的值赋值到变量中,如下:

1
2
3
4
5
6
.PHONY: all

OBJ = $(shell pwd)

all:
@echo "OBJ = $(OBJ)"

运行如下:

image-20250105221335105

3.5 foreach函数

如果想做一些循环或遍历操作时,可以使用foreach函数:

1
$(foreach VAR,LIST,TEXT)

foreach函数的工作过程是:把LIST中使用空格分割的单词依次取出并赋值给变量VAR,然后执行TEXT表达式。重复这个过程,直到遍历完LIST中的最后一个单词。函数的返回值是TEXT多次计算的结果。

举例:

1
2
3
4
5
6
7
8
.PHONY: all

SRC = main hello new

OBJ = $(foreach NEW,$(SRC),$(addsuffix .666,$(NEW)))

all:
@echo "OBJ = $(OBJ)"

运行如下:

image-20250105221747046

这只是举例而已,虽然加后缀的功能在本例中直接使用$(addsuffix .666,$(SRC))就可以完成,但是这只是举例$(foreach )可以遍历处理任务。

4 自定义函数

在实际使用中,常常可能因为便捷性的考虑,会新建一些函数,用于复用,然后通过$(call functionname, para1, para2,...)来调用,例如:

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

define my_function
@echo "hello in $(0)"
@echo "$(0) get first para is $(1) and $(2)"

endef

all:
$(call my_function, 666, 777)

运行如下:

image-20250105215617956

二、debug手段

打印变量

例如想查看某个变量是否被正确赋值,例如C文件搜索是否正确,或是CFLAGS是否符合要求,可以通过打印查看。以下三种打印区别就是:

  • $(info ):仅提示

  • $(warning ):报一个warning的提示,一般使用这个

  • $(error ):作为一个报错,同时停止运行,大型项目中可以更快定位

使用方式:

SRC := $(wildcard *.c)

$(warning CZR_Prefix-$(SRC))

注意:

添加CZR\_Prefix是为了更好地在console输出中定位文本信息。