1. 概述
  2. 书写规则
  3. 执行命令
  4. 使用变量
  5. 执行流控制
  6. 使用函数
  7. 隐式规则
  8. make 的运行
  9. 使用 make 更新函数库文件

1. 概述

主要参考地址: [跟我一起写 makefile | seisman 的博客]

首先,代码的编译过程分为两部分:编译,链接.
编译和链接需要指定编译命令,输入文件,输出文件等,如果文件很多的话,会很麻烦.
而 makefile 就是为了简化编译过程而出现的.
makefile 指定了一系列规则来指导编译和链接的过程.

makefile 的规则形如:

1
2
3
4
5
6
7
8
9
# 注释以 `#` 起始
# target为一个目标
# prerequisite为该目标依赖的其他目标项
# command为从依赖项生成目标项的命令
# 可以使用 `\` 主动指定换行
target : prerequisite ... \
prerequisite ...
command
...

其中目标可以是一个具体文件,也可以是一个动作,如clean动作
在使用 makefile 的时候,可以主动指定要生成的目标

1
2
3
4
5
6
# 主动声明哪些目标是动作而不是文件
.PHONY run debug clean
make run
make debug
make clean
make kernel

也可以直接运行 make 命令,使用默认的第一个目标
make 会自动递归寻找依赖目标并执行其命令,同时,makefile 会自动对比 target 文件和 prerequisite 文件的更新日期,如果目标依赖文件没有更新,则其会自动跳过该命令,使用上一次生成的目标

makefile 中可以定义变量,使用方法与 shell 中类似,同时 makefile 中还定义了一些预定义变量

1
2
3
4
5
6
7
# 定义变量
CC = gcc
C_FLAGS = $(INCLUDE_PATH) -c -fno-builtin -m32 -fno-stack-protector -g
# 使用变量
$(CC) $(C_FLAGS) -o $@ $<
# $@ --> target名
# $< --> 依赖名

使用include关键字引用其他 makefile(被引用文件会被原样复制到引用位置)
可以在执行 make 命令的时候使用-I参数指出引用目录

1
2
3
4
5
# 普通引用
include foo.make ./include/*.make $(files)
# 忽略错误引用
# 即如果没有找到相关文件,也不会抛出错误
-include error.make

总的工作流程:

  1. 读取所有的 makefile
  2. 执行include命令
  3. 初始化变量
  4. 推到隐式规则
  5. 为所有目标建立依赖关系链
  6. 决定哪些目标需要重新生成
  7. 执行生成命令

2. 书写规则

2.1 基本规则

首先基本的书写规则为:

1
2
3
target : prerequisite
command
...

2.2 文件搜索

在项目比较大的时候,可以设置 makefile 的搜索路径,即环境变量VPATH
同时,当前目录为最高优先级,其次为 VPATH 中的路径

1
2
# 路径优先级从头到尾递降,不同路径以冒号分割
VPATH=src:../headers

除了使用变量外,可以使用关键字vpath指定路径

1
2
3
4
5
6
7
8
9
10
11
12
# 为符合 pattern 的文件指定路径 directory
vpath <pattern> <directory>:<directory>:...
# 清除符合 pattern 的文件的 vpath 设置
vpath <pattern>
# 清除所有 vpath 设置
vpath

# 如果一个文件符合多个模式,则根据 vpath 声明顺序确定搜索顺序
# 一个 main.c 文件的搜索路径为: aoo -> boo -> coo -> doo
vpath %.c aoo:boo
vpath % coo
vpath %.c doo

2.3 伪目标

并非所有的目标都对应一个文件,有的目标可能对应一个动作

1
2
3
4
# 使用 .PHONY 指明哪些目标是伪目标(动作)
.PHONY : clean
clean :
rm *.o temp

2.4 多目标

有时候多个目标文件会依赖同一套依赖,同时命令也类似
此时可以使用多目标的方法简化操作

1
2
3
4
5
6
7
8
# 以下规则会自动展开成下面两个规则
outfile1 outfile2 : inputfile
gcc -o $@ inputfile
# 展开后的规则
outfile1 : inputfile
gcc -o outfile1 inputfile
outfile2 : inputfile
gcc -o outfile2 inputfile

2.5 静态模式

静态模式定义了一系列输入文件和输出文件类似时的处理规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# targets 为待处理的一系列输出文件,可以引用变量
# target-pattern 为一个匹配规则
# prereq-patterns 为转换规则
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...

# 如下规则,会自动展开为两条规则
objects = aoo.o boo.o coo.lib

$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@

# 展开后的规则
aoo.o : aoo.c
$(CC) -c $(CFLAGS) $< -o $@
boo.o : boo.c
$(CC) -c $(CFLAGS) $< -o $@

2.6 自动生成依赖性

具体参考:[makefile 自动生成依赖 | seisman 的博客]

编译的时候可以使用-M/-MM 选项来仅仅生成依赖目标

1
2
3
4
5
6
7
8
9
10
11
12
# 使用-MM选项会忽略标准库文件
[yishiyu-Lenovo:syscall]
[yishiyu]% gcc -MM ipc.c
ipc.o: ipc.c

# 使用-M选项会把标准库中的内容也放到输出结果中
# 如果不适用标准库文件而使用自己的,可以使用-I选项设置自己的文件搜索路径
ipc.o: ipc.c /usr/include/stdc-predef.h /usr/include/syscall.h \
/usr/include/x86_64-linux-gnu/sys/syscall.h \
/usr/include/x86_64-linux-gnu/asm/unistd.h \
/usr/include/x86_64-linux-gnu/asm/unistd_64.h \
/usr/include/x86_64-linux-gnu/bits/syscall.h

3. 执行命令

3.1 执行命令

makefile 中执行命令或执行 make 时有一些可以操作的选项

1
2
3
4
5
6
7
# 在命令中,可以加上#前缀来取消输出命令
target :
echo "AMD YES"
@echo "Intel YES"
> echo "AMD YES"
> AMD YES
> Intel YES
1
2
3
# 执行make命令的时候
# 加入-n指令可以仅打印构建命令而不执行(用于调试makefile)
# 加入-s指令可以禁止所有输出

makefile 中的命令会一条一条执行,在执行下一条指令的时候会恢复初始状态
(仅限更改了当前终端的命令如 cd,更改系统状态的命令除外如 mount)
如果希望把上一个命令的结果应用于下一条指令,可以把两条指令写在一行并用分号分隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
target:
pwd
cd test
pwd
cd test;pwd
# pwd
# /home/yishiyu/Code/notes/cpp/make
# cd test
# pwd
# /home/yishiyu/Code/notes/cpp/make
# cd test;pwd
# /home/yishiyu/Code/notes/cpp/make/test

# 在子文件夹中执行make命令(两种方式)
cd subdir;make
make -C subdir

正常情况下,如果 makefile 中的命令执行出错,会终止规则的运行
如果希望忽略命令的错误,可以在命令前添加一个-

1
2
clean:
-rm -f *.o

3.2 传递参数

makefile 可以实现分层设计(各个文件夹内的 makefile 控制本文件夹的构建行为)

1
2
3
4
5
6
7
# 显式声明把变量传递给下一级makefile
export <variable ...>
# 显式声明不把变量传递下去
unexport <variable ...>

# 还可以把变量声明和传递语句合并
export DIR=./include

3.3 定义命令包

可以使用定义命令包的方式来为所有的重复命令序列定义一个模板

1
2
3
4
5
6
7
8
# 定义一个名字为 compile-lib 的命令包
define compile-lib
gcc $(CPP_FLAGS) -o $< $@
endef

# 使用命令包
string.o : string.c
$(compile-lib)

4. 使用变量

4.1 变量的声明

makefile 中有四种变量声明方式

赋值方式 作用
简单赋值( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效.
递归赋值( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响.
条件赋值( ?= ) 如果变量未定义,则使用符号中的值.如果该变量已经赋值则无效.
追加赋值( += ) 原变量用空格隔开的方式追加一个新值.

4.2 变量的高级用法

  1. 变量替换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 第一种方法,所有被匹配到的地方会被替换
    # ao bo co
    foo := a.o b.o c.o
    bar := $(foo:.o=.c)

    # 第二种方法,使用模式进行匹配
    # x123y x234y xjkhy
    src := a123b.c a234b.c ajkhb.c
    obj := $(src:a%b.c=x%y)
  2. 变量寻址

    1
    2
    3
    4
    5
    6
    # 可以把一个变量的值当做地址进行寻址
    # 最后的结果是 a=u
    x = y
    y = z
    z = u
    a := $($($(x)))
  3. 变量进行组合(可以和变量寻址组合使用)

    1
    2
    3
    4
    5
    # 最后的结果为all=Hello
    first_second = Hello
    a = first
    b = second
    all = $($(a)_$(b))
  4. 使用命令行设置参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    all=123
    target:
    @echo ${all}
    # 命令行中可以传入参数(会覆盖同名变量,但不同实现也可能不覆盖)
    make all=456
    # 可以使用 override 关键字主动指定该变量不被覆盖
    override all=123
    target:
    @echo ${all}
  5. 多行变量

    1
    2
    3
    4
    5
    # 可以使用 define 关键字定义多行变量(和命令包是同一个关键字)
    define val
    aoo.c
    boo.c
    endef
  6. 目标变量(即这个变量仅对一个目标生效)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # CFLAGS 仅对prog这个目标生效
    prog : CFLAGS = -g
    prog : prog.o foo.o bar.o
    $(CC) $(CFLAGS) prog.o foo.o bar.o

    prog.o : prog.c
    $(CC) $(CFLAGS) prog.c

    foo.o : foo.c
    $(CC) $(CFLAGS) foo.c

    bar.o : bar.c
    $(CC) $(CFLAGS) bar.c

    # 通用格式如下,同时对于目标可以使用模式进行匹配
    <target ...> : <variable-assignment>;
    <target ...> : overide <variable-assignment>

    5. 执行流控制

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
# 通用语法如下
# 其中<conditional-directive>关键字共有四种:ifeq ifneq ifdef ifndef
# 具体语法格式如下:
# ifeq (<arg1>, <arg2>)
# ifeq '<arg1>' '<arg2>'
# ifeq "<arg1>" "<arg2>"
# ifneq "<arg1>" '<arg2>'
# ifneq '<arg1>' "<arg2>"
# ifdef foo
# ifndef foo
<conditional-directive>
<code>
# else
# <code>
endif

# 示例如下:
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif

ifdef foo
frobozz = yes
else
frobozz = no
endif

6. 使用函数

makefile 中使用函数的通用格式如下,其中参数可以有多个,以逗号分隔

1
$(<function> <arguments>)

6.1 字符串处理函数

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
# 1. 字符串替换
# from --> 替换前字符串
# to --> 替换后字符串
# text --> 目标字符串
$(subst <from>,<to>,<text>)

# 2. 模式替换
# 与 subst 类似
$(patsubst <pattern>,<replacement>,<text>)

# 3. 清除头尾空格
$(strip <string>)

# 4. 查找子字符串
# 如果在in中找到find,则返回find,否则返回空字符串
$(findstring <find>,<in>)

# 5. 过滤字符串
# 仅保留符合模式的字符串,可以有多个模式
$(filter <pattern...>,<text>)

# 6. 反过滤字符串
# 过滤掉符合模式的字符串
$(filter-out <pattern...>,<text>)

# 7. 排序函数
# 返回升序字符串序列
$(sort <list>)

# 8. 取单词函数
# 提取text中的第n个单词
$(word \<file\>,<text>)

# 9. 取但单词串函数
# ss为起始下标,e为结束下标(最多返回所有单词,最少返回空字符串)
$(wordlist <ss>,<e>,<text>)

# 10. 单词计数函数
# 统计text中的单词个数
$(words <text>)

# 11. 取首个单词
# 相当于$(word 1,<text>)
$(firstword <text>)

6.2 文件名操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 取目录函数
# 处理names中的每一个序列,抽取出各个文件的目录
$(dir <names...>)

# 2. 取文件函数
# 处理names中的每一个序列,抽取出不含路径的文件名
$(notdir <names...>)

# 3. 取后缀函数
$(suffix <names...>)

# 4. 取前缀函数
$(basename <names...>)

# 5. 加后缀函数
$(addsuffix <suffix>,<names...>)

# 6. 加前缀函数
$(addprefix <prefix>,<names...>)

# 7. 拼接函数
# list1中的单词依次与list2中的单词拼接后返回
$(join <list1>,<list2>)

6.3 其他函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. foreach函数
# list中的所有变量依次复制给变量var
# 对于var变量执行表达式text并返回一个结果(字符串)
# 整个函数返回text返回的所有字符串(以空格分割)
$(foreach <var>,<list>,<text>)

# 2. 条件函数
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)

# 3. call函数
# 以parm1,parm2...初始化expression中的$(1),$(2)...
# 返回expression的返回值
$(call <expression>,<parm1>,<parm2>,...,<parmn>)

# 4. shell函数
# 直接使用shell函数
$(shell <command> <parm...>)

# 5. 控制函数
# 抛出警告或者错误
$(warning <text ...>)
$(error <text ...>)

7. 隐式规则

7.1 隐式规则推导

仅关注 C 和 C++的隐式推导

  1. 编译 C 程序的隐含规则

    \.o 的目标的依赖目标会自动推导为 \.c ,并且其生成命令是
    $(CC) –c $(CPPFLAGS) $(CFLAGS)

  2. 编译 C++程序的隐含规则

    \.o 的目标的依赖目标会自动推导为 \.cc 或是 \.C ,并且其生成命令是
    $(CXX) –c $(CPPFLAGS) $(CFLAGS)
    建议使用 .cc 作为 C++源文件的后缀,而不是 .C

7.2 默认变量

  1. 默认程序变量

    | 变量 | 默认值 | 含义 |
    | :—- | :——- | :———————- |
    | AS | as | 汇编语言编译程序 |
    | CC | cc | C 语言编译程序 |
    | CXX | g++ | C++编译程序 |
    | RM | rm -f | 删除文件命令 |
    | AR | ar | 函数库打包程序 |

  2. 默认参数变量

    | 变量 | 含义 |
    | :———- | :————————- |
    | ASFLAGS | 汇编语言编译器参数 |
    | CFLAGS | C 语言编译器参数 |
    | CXXFLAGS | C++语言编译器参数 |
    | CPPFLAGS | C 预处理器参数 |

7.3 自动化变量

变量 含义
$@ 目标文件集合
$% 目标中的函数库文件
$< 依赖中的第一个依赖文件名
$? 所有比目标新的依赖目标的集合
$^ 所有的依赖目标的集合(去重)
$+ 所有的依赖目标的集合(不去重)

可以给上述变量加上DF以表示取路径或文件

1
2
$(@D):取目标文件的路径
$(@F):取目标文件的文件

8. make 的运行

主要是命令行中的 make 参数设置

选项 含义
-n 不执行命令只打印,用于调试 makefile
-s 只执行命令不打印
-t 只把目标文件时间更新而不实际重新生成
-q 查询目标是否需要更新(0->不需要;1->需要;2->目标错误)
-b 忽略和其他版本 make 的兼容性
-B 忽略目标文件时间差异,编译所有需要的文件
-C 指定 makefile 的文件夹
-debug=[options] 指定输出信息等级(a,v,b)
-d -debug=d 的缩写
-f=[file] 指定 makefile
-i 忽略所有错误
-I 添加 makefile 搜索目录(可以叠加)
-j [N] 指定同时运行的命令个数(没有这个选项允许无限个)
-r 禁止使用所有隐式规则
-R 仅是使用作用于变量的隐式规则

9. 使用 make 更新函数库文件

(略,从来没用过,以后用到再学)

参考博客:[跟我一起写 Makefile | seisman 的博客]