Makefile 速查笔记

做 Linux C++,一个稳定的工程,Makefile 是很少改动的。但是如果需要修改的时候,Makefile 的语法和用法一时半会就回忆不出来(原谅我记忆力差……)。在此把自己以前的 Makefile 学习笔记记录一下吧,也作为分享。本文假设读者已经懂得了 Makefile,因此主要是作为备忘和速查用。

全文中尖括号部分表示变量。本文地址:https://segmentfault.com/a/1190000012091117

另外,速查系列还有这一篇:正则表达式速查笔记

Make 介绍

Makefile 的基本规则就是:

target ...: prerequirements ...
    command ...
    ...

其中 target 是目标文件,可以有多个,可以是 .o 文件或者是可执行问价,甚至可以是一个标签。
Prerequisites 是先决条件,可以是文件,也可以是另一个 target。
这就组成了一个依赖关系target 的先决条件定义在 prerequisites 中,而其生成规则又是由 command 决定的。如果包含多个规则的话,那么第一条规则就是整个 Makefile 的默认规则


Make 的工作流程

  1. 在当前目录中查找 Makefile 或者 makefile 文件
  2. 将文件中第一条规则作为默认规则
  3. 如果目标不存在,则寻找对应的 .o 文件
  4. 如果 .o 文件不存在,则寻找 .o 的依赖关系以生成它

Makefile 有很多默认的生成规则,但是本文我们不关心,因为绝大部分情况下,我们是需要自行写规则的。这便于自定义、便于移植、便于交叉编译、便于调试。


Makefile 中的变量

变量的定义和调用格式:

name = value    # 注意变量的值是允许空格的
$(name)

变量值的部分可以使用换行符 "\" 来做假换行,将两行内容连接成一行,从而缩短 Makefile 文件的宽度。

Makefile 综述

Makefile 里面都有啥?

文件指示:在一个 Makefile 里面可以制定另一个 makefile,类似于 C 的 include

Makefile 还可以做条件包含动作,类似于 #if。Makefile 可以定一个变量为一个多行的命令。

Makefile 里面只有行注释而没有段注释。注释采用 # 开头。如果要使用 # 字符,则需要转义,写成 “\#”。

Makefile 规则内容里所有的 shell 命令都要以制表符 Tab 开头,注意,空格符是不行的。

默认的 make 文件名为:GNUmakefile, makefile, Makefile,当敲入 make 命令时,会自动搜寻这几个文件。约定俗成使用最后一个。

引用其他 Makefile

语法:

include filename ...        # 不允许 include 失败
-include filename ...       # 允许 include 失败

可以包含路径或者通配符,一行可以包含多个文件。

如果未指定绝对路径或者相对路径,那么 make 会按照一下的顺序去寻找:

  1. 当前目录
  2. 制定 make 时,在 -I 或者 --include-dir 的参数下寻找
  3. <prefix>/include(一般是 /usr/local/bin/usr/include

建议还是手动指定吧,自动搜寻意外可能太多了。

环境变量 MAKEFILES

这里主要是要提醒:不要设置这个环境变量,否则会影响全局的 make include 动作。

Make 的几个工作方法

通配符

Make 支持三个通配符:*, ?, [...]。可以用在规则中,也可以用在变量中。

伪目标

伪目标就是 Makefile 里面颇为常见的 .PHONY 标识,比如:".PHONY: clean",表示这个规则名并不代表一个真实存在的、需要生成的文件名,而只是一条纯粹的规则。

  • 真目标的特点是:如果目标不存在,才会被执行
  • 伪目标的特点是:无视目标是否存在,必然执行

除了 make clean 之外,伪目标还有另一种使用场景,就是一个 make 动作,实际上生成了多个目标。比如:

.PHONY: all
all: exe install    # 包含了生成目标文件,以及安装动作

多目标

规则的冒号前面可以有多个 target,表示多个 target 共用这条规则。


自动生成依赖关系

如果我们使用中规中矩的 makefile 写法,那么对于每个源文件都要好好写头文件依赖关系,从而在头文件更新的时候,可以自动重新编译依赖于这个头文件的源文件。
这实在是太麻烦了。好在 gcc 里有一个 -MM(注意不是 “-M”) 的选项,可以分析出 .c 文件依赖的头文件并且打印出来。因此制作 Makefile 的时候,就可以利用这一特性自动生成依赖。

实现方法有很多,这里贴出我自己使用的例子,也可以参见我的工程代码

EXCLUDE_C_SRCS =#
C_SRCS = $(filter-out $(EXCLUDE_C_SRCS), $(wildcard *.c))
C_OBJS = $(C_SRCS:.c=.o)

$(C_OBJS): $(C_OBJS:.o=.c)
    $(CC) -c $(CFLAGS) $*.c -o $*.o
    @$(CC) -MM $(CFLAGS) $*.c > $*.d  
    @mv -f $*.d $*.d.tmp  
    @sed -e 's|.*:|$*.o:|' < $*.d.tmp > $*.d  
    @sed -e 's/.*://' -e 's/\\$$//' < $*.d.tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $*.d
    @rm -f $*.d.tmp 

书写命令

这里的命令,指的是在 Makefile 规则里的 “command” 部分。

命令执行

将 “@” 放在一条命令的前面,表示实际执行的时候,不打印这条命令语句,可以节省屏幕内容,减少垃圾信息(特别是我的自动生成依赖的命令,调通了之后,那就是一堆无用信息)。如果将 “-” 放在命令前面,则表示无视这条命令的返回值是否为成功(0).

如果上一条命令的结果需要用于下一条命令时,需要将这些命令写在一行中。建议用 “\” 分开。这最典型的是 cd 命令及其之后的一连串命令。

Make 的时候加上 -n 选项或 --just-print 选项,则表示不执行 make,而只是把过程打印出来。


嵌套执行 make

在 Makefile 里可以到另一个目录下执行 make,执行方式类似于普通的命令调用,但特别的是,make 可以识别出这是一条嵌套 make 指令,从而在 shell 中打印出 “专项哪里哪里 make” 的提示语法为:

subsystem:
    $(MAKE) -C subdir

这个做法的主要好处是可以向下级 Makefile 传递变量或者语法:

export VARIABLE ...     # 将相应变量变成当前 make 操作的全局变量

也恶意直接指定变量的值:

export VARIABLE = value

如果要传递所有变量(不推荐),直接写 export 就好。

注意由两个系统变量 SHELLMAKEFLAGS 是永远传递的。
此外还有一个全局变量 MAKELEVEL 用来表示当前的嵌套层数。


定义命令包

命令包类似于宏、子函数等等。使用 define 来定义,以 endif 结束,比如:

define run-yacc
    yacc $(firstword $^)
    mv y.tab.c $@
endif

注意如果是命令的话,需要以制表符开头。调用这个命令包的方式为:$(run-yacc)

使用变量

变量赋值

变量定义时必须赋值,至少赋一个空值(只有等号,等号右边什么都没有)

使用变量的时候虽然不强制、但是为了安全起见,应该使用括号或者打括号把变量包含起来。如果要使用字符 "$",则使用 "$$" 来转义

赋值时,等号右侧可以有未定义的变量,并且在其实际使用时才展开变量的内容。但这可能会导致循环引用。为了避免这一点,可以使用 ":=" 符号来避免使用未定义的变量

"+=" 的作用是 “追加” 值。如果右侧有变量未定义,则等价于 “:=
"?=" 的作用是:如果等号左侧的变量未定义,则使用等号右边内容定义,即:

ifeq ($(some_var), undefined)
some_var = some_val
endif

另外:
$@ 表示当前规则的编译目标
$^ 表示当前规则的所有依赖文件
$$< 表示当前规则的第一个依赖。


定义一个空格变量

NULL_STR :=#
SPACE_STR := $(NULL_STR) # end of line

注意第二行的注释与 “)” 之间是包含一个空格的。注释的 “#” 必须有,否则不会定义一个空格出来。


变量替换

第一个方式为:$(var: .o = .c),意思是将等号左边的字符换成右边的字符
第二个方式为所谓的 “静态模式”:$(var: %.o = %.c)


把变量值作为变量

这很类似于指针,只是地址值变成了变量值。可以用变量值生成变量名,比如:a := $($(var)) 或者是 $($(var)_$(idx)) 之类的写法。


override

在命令行调用 make 时,可以直接指定某个变量的全局值,使得它在整个 make 的过程中一直不变。为了防止这个特性,可以使用这个关键字来处理:
override <variable> = <value>
等号也可以用 :=?=


目标变量(局部变量)

如果某条约束里面不想使用已经定义了的全局变量,可以这样写:

prog: CFLAGS = -g
prog: a.o b.o
    $(CC) $(CFLAGS) a.o b.o

条件判断

语法

<条件语句>
<true 执行语句>
else
<false 执行语句>
endif

其中条件语句有四种情形:

1、表示是否相等

    ifeq (<arg1>, <arg2>)    # 推荐
    ifeq '<arg1>' '<arg2>'
    ifeq "<arg1>" "<arg2>"

2、表示是否不等,上面的 ifeq 换成 ifneq
3、ifdef
4、ifndef

使用函数

Make 的所有函数都是内置函数,不能自己定义(命令包除外)。下面列出常用的函数,如果看不懂再详细查阅。

常规函数

字符串替换

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

模式字符串替换

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

去开头和结尾的空格

$(strip <string>)

查找字符串

$(findstring <find>, <in>)

反过滤

$(filter-out <pattern_or_string>, <text>)

排序(单词升序)

$(sort <list>)

取单词

$(word <n>, <text>)

取单词串

$(wordlist <n_start>, <n_end>, <text>)

单词个数统计

$(words <text>)

去掉每个单词的最后文件名部分,只剩下目录部分

$(dir <names ...>)

去掉每个单词的目录部分,只剩下文件名部分

$(notdir <names ...>)

读取各文件名的后缀

$(suffix <names ...>)

加后缀

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

加前缀

加前后缀在动态创建局部变量很有用
$(addprefix <prefix>, <names ...>)

连接字符串

$(join <list1>, <list2>)


for 循环

$(foreach <var>, <list>, <text>)
这其实是一个函数,作用是:将 list 的单词逐一取出,放到 var 指定的变量中,然后执行 text 的表达式。返回值则是 text 的最终执行值。


Shell 函数

执行 shell 命令,并且将 stdout 作为返回值返回,如:
contents := $(shell ls -la)


控制 make 输出

$(error <text ...>)
$(warning <text ...>)
这也同时是调试和定位 make 的好方法。


判断文件是否存在

ifeq ($(FILE), $(wildcard $(FILE)))
...
endif
阅读 4.8k

推荐阅读

曾经的嵌入式 / 后台开发一枚,现在开始走向架构。本专栏没有高深技术,只讲基础组件、工具,请放心食用

76 人关注
84 篇文章
专栏主页