从零到一的makefile

笔者上手一些大型项目时,常常会看不懂Makefile而造成一些困难,因此参考资料形成了一篇文章,从完全零基础过来的,用时也不多。文末附上一个笔者最近学习的rCore的Makefile,看完全篇文章后一定可以看懂的,本文也可用于一个简单的中文手册查询。

一些好的学习资料:

Makefile Tutorial By Example

【从零开始学Makefile】 从零开始学Makefile_哔哩哔哩_bilibili

Make/make.md · 岩木/CPP - Gitee.com

跟我一起写Makefile — 跟我一起写Makefile 1.0 文档 (seisman.github.io)

makefile有点像跟手动编译过程反着来,一步步去找依赖项

规则的构成

1
2
3
4
targets: prerequisites
command
command
command
  • targets(目标):是文件名,通常一个规则只有一个targets

  • command(命令/方法):创建目标的一系列步骤,需要用tab开头,

  • prerequisites(依赖):也是文件名,可以有多个,用空格分隔,在运行目标的命令之前,这些文件需要存在。

命令和执行

命令执行

command其实就是执行shell的指令,默认一个command都是一个独立的shell来执行,如果需要所有command都在一个shell执行,需要.ONESHELL

一个例子

1
2
3
4
5
6
7
8
9
10
11
all: 
cd ..
#上面的cd不会影响这个echo,因为每行命令都在一个独立的shell里面运行
echo `pwd`

# 这个cd会影响echo,因为他们都在同一行,因此会打印出上级目录的路径
cd ..;echo `pwd`

# 和第二个一样,\表示在同一行
cd ..; \
echo `pwd`

command回显

command默认是先打印出语句,再执行内容,如果不需要先打印语句,在command前面加上@

1
2
a: a.o
@echo hello

也可以使用.SILENT + 文件名,表示直接执行不打印

错误处理

如果一条规则当中包含多条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

也可以make -k,即使遇到错误也能继续执行,如果想一次查看Make的所有错误,可以使用-k

目标

在终端执行命令时,如果没有将目标作为 make 参数提供给命令,将运行第一个目标

make只会在这两种情况运行targets及其命令

  • targets不存在,还未被创建
  • targets的依赖项更新了 (使用文件系统时间戳作为代理来确定是否有任何变化)

一个示例

1
2
3
4
5
6
7
8
9
blah: blah.o
cc blah.o -o blah # Runs third

blah.o: blah.c
cc -c blah.c -o blah.o # Runs second

# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
echo "int main() { return 0; }" > blah.c # Runs first

以下 Makefile 最终运行所有三个目标。当您在终端中运行时 make ,它将构建一个按一系列步骤调用 blah 的程序:

  • Make 选择目标,因为第一个目标是默认目标 blah
  • blah 需要 blah.o ,因此搜索 blah.o 目标
  • blah.o 需要 blah.c ,因此搜索 blah.c 目标
  • blah.c 没有依赖项,因此运行命令 echo
  • 然后运行该 cc -c 命令,因为所有 blah.o 依赖项都已完成
  • 将运行 顶部 cc 命令,因为所有 blah 依赖项都已完成
  • 就是这样: blah 是一个编译好的c程序

可以看出targets会事先搜索依赖项是否已经创建,如果一个targets没有依赖项,那么会直接运行

all

运行all后面的所有目标

1
2
3
4
5
6
7
8
9
10
11
all: one two three

one:
touch one
two:
touch two
three:
touch three

clean:
rm -f one two three

一个规则多个目标

当一个规则有多个目标时,将针对每个目标运行,$@是包含目标名称的auto 变量

1
2
3
4
5
6
7
8
9
all: f1.o f2.o

f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o

同一目标多条规则

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

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

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
4
5
6
7
all: one = cool

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)
  • other目标看到的$one会打印一个空串

特定模式

1
2
3
4
5
6
7
%.c: one = cool

blah.c:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

依赖

普通依赖

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

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

order-only依赖

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

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

定义方法如下:

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

normal-prerequisites部分可以为空

变量

变量定义类似C语言里的宏展开,只是字符串替换,因此变量只有一种类型:字符串

变量只能是字符串。使用:==

$(x)就是用x这个字符串变量的值来替换$(x)

${x}$(x)等价

1
2
3
files := file1 file2
some_file:
echo "Look at this variable: " $(files)

会打印出Look at this variable: file1 file2

$符号

如果想表示$这个符号

1
2
# a 是 $b 这个字符串
a = $$b

例子:

1
2
3
4
5
6
7
make_var = I am a make variable
all:
# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
sh_var='I am a shell variable'; echo $$sh_var

# Same as running "echo I am a make variable" in the shell
echo $(make_var)
  • 对于echo $$sh_var,实际上等价于echo $sh_var
  • echo $(make_var),实际上等价于echo I am a make variable

通配符

wildcard(通配符)

wildcard函数:用于匹配文件名模式,可与*?配合使用

1
2
3
4
5
6
# 查找当前目录下所有的 .c 文件
C_FILES := $(wildcard *.c)

# 打印找到的 .c 文件列表
all:
@echo "C files: $(C_FILES)"

*:在文件系统中搜索匹配的文件名,匹配0或多个字符

  • 注意不要在变量定义中使用*
  • *没有匹配任何文件时,它将保持原样(除非在wildcard函数中运行)
  • 建议*永远与wildcard配合使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)

all: one two three four

# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)

# Stays as *.o if there are no files that match this pattern :(
two: *.o

# Works as you would expect! In this case, it does nothing.
three: $(thing_right)

# Same as rule three
four: $(wildcard *.o)

%

  • 当在匹配模式下使用时,会匹配字符串中的一个或多个字符
  • 当在替换模式下使用时,会采用匹配的结果并将其替换在字符串中
  • 常用于规则定义和特定函数

auto自动变量

$@:本条规则的目标名

$?:依赖中修改过的文件名

$^:所有依赖文件名,文件名不会重复,不包含order-only依赖 (就是|右边的)

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

伪目标 .PHONY

如果一个目标并不是一个文件,则这个目标就是伪目标。例如前面的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

Makefile读取过程

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

  • 读取Makefile文件的所有内容
  • 根据Makefile的内容在程序内建立起变量
  • 在程序内构建起显式规则、隐式规则
  • 建立目标和依赖之间的依赖图

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

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

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

  • 在一个立即展开的表达式中用到
  • 在第二个阶段中用到

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

一个例子:

1
2
3
4
5
6
7
8
9
a = ok

#file是=,所以延迟展开
file = $(a)

all:
@echo $(file)
# a在第一阶段被覆盖为no,最后echo出no
a = no

变量赋值

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

第一种方式就是直接使用=,这种方式如果赋值的时候右边是其他变量引用或者函数调用之类的,将不会做处理,直接保留原样,在使用到该变量的时候再来进行处理得到变量值(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

Shell运行赋值

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

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

追加

使用 += 用于追加

1
2
3
4
5
foo := start
foo += more
# foo变为 start more,中间有个空格
all:
echo $(foo)

取消变量

如果想清除一个变量,用以下方法,变量就会变为空

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

变量替换引用

语法:**$(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)

这样即使命令行指定参数,也只会为ThisIsInMakefile

1
make OverridDemo=ThisIsFromOutShell

绑定目标的变量

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
targets …: target-pattern(目标模式): prereq-patterns(依赖模式) …
recipe

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

条件判断

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,用法相同,只是结果相反

ifdef判断变量是否已经定义

ifdef不展开变量,他只是查看是否定义了变量

1
2
3
4
5
6
7
8
9
10
bar =
foo = $(bar)

all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif

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

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

函数

调用函数的语法

$(fn, arguments)${fn, arguments},注意参数之间不要有空格,如果有空格的话将视为参数的一部分

字符替换与分析

subst

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

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



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

all:
@echo $(srcs)
@echo $(headers)
  • 如果要替换空格或者逗号,使用变量

    1
    2
    3
    4
    5
    6
    7
    8
    comma := ,
    empty:=
    space := $(empty) $(empty)
    foo := a b c
    bar := $(subst $(space),$(comma),$(foo))

    all:
    @echo $(bar)

patsubst

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

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


foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)

all:
echo $(one)
echo $(two)
echo $(three)
  • 注意上面twothree的简写,也很常用

strip

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

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


files = hello.cpp main.cpp test.cpp
files2 = $(strip $(files))

findstring

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

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

files = hello.cpp main.cpp test.cpp
#返回"hel"
find = $(findstring hel,$(files))
#返回空串
find = $(findstring HEL,$(files))

filter

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

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

files = hello.cpp main.cpp test.cpp main.o hello.o hello.h
#files2: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开始的,不是0)

1
2
3
4
5
6
7
$(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:test.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:src/ ./
files2 = $(dir $(files))

notdir

返回除目录部分的文件名

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

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

suffix

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

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


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

basename

返回文件名除后缀的部分

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


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

addsuffix

给文件名添加后缀

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

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

addprefix

给文件名添加前缀

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

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

join

将两个列表中的内容一对一连接,如果两个列表内容数量不相等,则多出来的部分原样返回,注意是有顺序之分的,比如下面这个例子如果改为files2 = $(join $(f2),$(f1)),结果是相反的

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


f1 = hello main test
f2 = .cpp .hpp
#files2:hello.cpp main.hpp test
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

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
8
$(or condition1[,condition2[,condition3…]])

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

and

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

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

f1 = 12
f2 = 34
f3 = hello.cpp
f4 = main.cpp
#files2: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
8
9
10
11
12
13
14
$(foreach each,list,process)
--- each list中的每一项
--- list 需要处理的字符串序列,用空格隔开
--- process 需要对每一项进行的处理

foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)
all:
# Output is "who! are! you!"
@echo $(bar)

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

  • Make支持创建函数,可以通过创建变量来定义函数,但使用参数 $(0)$(1) 等,$(0) 是变量,而 $(1)$(2) 等是参数

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

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

sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
# Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
@echo $(call sweet_new_fn, go, tigers)

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目标,输出Target Eval Test

shell

用于执行Shell命令

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

let

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

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 编译结束)

实战

试试分析一下这个rust的makefile把,来自rCore

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
TARGET := riscv64gc-unknown-none-elf
MODE := release
APP_DIR := src/bin
TARGET_DIR := target/$(TARGET)/$(MODE)
BUILD_DIR := build
OBJDUMP := rust-objdump --arch-name=riscv64
OBJCOPY := rust-objcopy --binary-architecture=riscv64
PY := python3

ifeq ($(MODE), release)
MODE_ARG := --release
endif

BASE ?= 0
CHAPTER ?= 0
TEST ?= $(CHAPTER)

ifeq ($(TEST), 0) # No test, deprecated, previously used in v3
APPS := $(filter-out $(wildcard $(APP_DIR)/ch*.rs), $(wildcard $(APP_DIR)/*.rs))
else ifeq ($(TEST), 1) # All test
APPS := $(wildcard $(APP_DIR)/ch*.rs)
else
TESTS := $(shell seq $(BASE) $(TEST))
ifeq ($(BASE), 0) # Normal tests only
APPS := $(foreach T, $(TESTS), $(wildcard $(APP_DIR)/ch$(T)_*.rs))
else ifeq ($(BASE), 1) # Basic tests only
APPS := $(foreach T, $(TESTS), $(wildcard $(APP_DIR)/ch$(T)b_*.rs))
else # Basic and normal
APPS := $(foreach T, $(TESTS), $(wildcard $(APP_DIR)/ch$(T)*.rs))
endif
endif

ELFS := $(patsubst $(APP_DIR)/%.rs, $(TARGET_DIR)/%, $(APPS))

binary:
@echo $(ELFS)
@if [ ${CHAPTER} -gt 3 ]; then \
cargo build $(MODE_ARG) ;\
else \
CHAPTER=$(CHAPTER) python3 build.py ;\
fi
@$(foreach elf, $(ELFS), \
$(OBJCOPY) $(elf) --strip-all -O binary $(patsubst $(TARGET_DIR)/%, $(TARGET_DIR)/%.bin, $(elf)); \
cp $(elf) $(patsubst $(TARGET_DIR)/%, $(TARGET_DIR)/%.elf, $(elf));)

disasm:
@$(foreach elf, $(ELFS), \
$(OBJDUMP) $(elf) -S > $(patsubst $(TARGET_DIR)/%, $(TARGET_DIR)/%.asm, $(elf));)
@$(foreach t, $(ELFS), cp $(t).asm $(BUILD_DIR)/asm/;)

pre:
@mkdir -p $(BUILD_DIR)/bin/
@mkdir -p $(BUILD_DIR)/elf/
@mkdir -p $(BUILD_DIR)/app/
@mkdir -p $(BUILD_DIR)/asm/
@$(foreach t, $(APPS), cp $(t) $(BUILD_DIR)/app/;)

build: clean pre binary
@$(foreach t, $(ELFS), cp $(t).bin $(BUILD_DIR)/bin/;)
@$(foreach t, $(ELFS), cp $(t).elf $(BUILD_DIR)/elf/;)

clean:
@cargo clean
@rm -rf $(BUILD_DIR)

all: build

.PHONY: elf binary build clean all

从零到一的makefile
http://example.com/2024/03/07/makefile/
Author
Jianhui Yin
Posted on
March 7, 2024
Licensed under