Makefile 的文件名

默认情况下,make 命令会在当前目录下按顺序寻找文件名为 “GNUmakefile”、“makefile”、“Makefile” 的文件,找到了解释这个文件。

在这三个文件名中,最好使用 “Makefile” 这个文件名,因为,这个文件名第一个字母为大写,比较醒目。最好不使用 “GNUmakefile”,这个文件是 GNU 的 make 识别的。有另外一些 make 只对全小写的 “makefile” 文件名敏感,但基本上来说,大多数的 make 都支持 “makefile” 和 “Makefile” 这两种默认文件名。

当然,可以使用别的文件名来书写 Makefile,比如 “Make.Linux”,这样就要使用 “-f” 或 “–file” 参数,如:make -f Make.Linuxmake --file=Make.Linux

Makefile 里有什么?

Makefile 里主要包含了五个东西:显式规则、隐式规则、变量定义、文件指示、注释。

  1. 显式规则。说明了如何生成一个或多个目标文件。显式地指出,要生成的文件、文件的依赖、生成的命令。
  2. 隐式规则。由于 make 有自动推导的功能,所以隐式规则可以让我们比较粗糙地简略地书写 Makefile。
  3. 变量定义。变量一般都是字符串,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。包括三部分
    1. 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 include 一样;
    2. 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样;
    3. 定义一个多行的命令。
  5. 注释。Makefile 中只有行注释,和 Shell 脚本一样,注释使用 “#” 字符。

规则

targets : prerequisites
	command
	...
  1. 目标(targets), 是文件名或标签,用空格分开,可以使用通配符。

  2. 前置条件(prerequisites), 是目标依赖的文件(或依赖的目标)。如果其中的某个文件比目标文件新,那么,目标就被认为是 “过时的”,被认为是需要重新生成的。

  3. 命令(command), 是命令行,必须以 Tab 键开头。

如果一行太长,可以使用 “\” 作为换行符。

目标是必须的,不可省略;前置条件命令 都是可选的,但是两者之中必须至少存在一个。

每条规则就明确两件事:

  1. 构建目标的前置条件是什么
  2. 如何构建

隐式规则

隐式规则可以说就是没有规则。(注意和通配符区分)

make 会试图自动推导产生这个目标的规则和命令。

例如:

main : main.o foo.o
	gcc -o main main.o foo.o $(CFLAGS) $(LDFLAGS)

上面是一个完整的 Makefile 内容,可以看到,文件并没有写如何生成 main.o 和 foo.o 的规则(目标、依赖、命令)。但是 make 会自动推导这两个目标的依赖和生成命令。

make 会在自己的 “隐式规则” 库中寻找可以用的规则。如果找到,那么就会使用;如果找不到,那么就会报错。

在上面的例子中,make 调用的隐含规则是,把 .o 的目标依赖文件设置成 .c ,并使用 C 的编译命令 cc -c $(CFLAGS) main.c 来生成 main.o 。规则如下

main.o : main.c
	cc -c -o main.o main.c $(CFLAGS)
foo.o : foo.c
	cc -c -o foo.o foo.c $(CFLAGS)

所以,我们完全没有必要显式地写出上面两条规则。因为,这是已经“约定”号了的事情。

make 和我们约定好了用 C 编译器 cc 生成 .o 文件的规则,这就是隐式规则。

当然,如果我们为 .o 文件书写了自己的规则,那么 make 就不会自动推导并调用隐式规则,它会按照我们写好的规则执行。

变量

变量在声明时需要给予赋值,在使用时需要在变量名前加上 $ 符号,最好使用小括号 () 或大括号 {} 把变量名包括起来。

调用 Shell 变量,需要在 $ 符合前面在加一个 $,因为 make 命令会对 $ 符号转义。如

test :
	echo $$HOME

有时,变量的值可能指向另一个变量。

v1 = $(v2)

上面代码中,变量 v1 的值是另一个变量 v2 的值。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值时动态的,这两种扩展方式的结果可能会不一样。

为了解决这类问题,Makefile 一共提供了四个赋值运算符 =:=?=+=。它们的区别如下:

VARIABLE = value
# 在执行时扩展,允许递归扩展。

VARIABLE := value
# 在定义时扩展。

VARIABLE ?= value
# 只有在该变量为空时才设置值。

VARIABLE += value
# 将值追加到变量的尾端。

内置变量

make 命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的 make 工具。

自动变量

  • $@ 指代当前目标。
  • $^ 指代所有前置条件,之间以空格分隔。
  • $< 指代第一个前置条件。

判断

ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif

循环

LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

函数

调用函数的格式

$(function arguments)

内置函数

shell 函数

用来执行 shell 命令

files = $(shell ls)
test :
	@echo $(files)

wildcard 函数

替代 bash 展开的结果

srcfiles = $(wildcard *.c)
test :
	@echo $(srcfiles)

执行结果

$ make test 
foo.c main.c

patsubst 函数

用于模式匹配的替换

srcfiles = $(wildcard *.c)
objects = $(patsubst %.c, %.o, $(srcfiles))
test2 :
	echo $(objects)

命令

命令(commands)表示如何更新目标文件,由一行或多行 shell 命令组成。它是构建“目标”的具体指令,它的运行结果通常就是生成目标文件。

  • 每行命令前必须使用 Tab 键。
  • 每行命令在一个单独的 shell 中执行,这些 shell 之间没有继承关系。(但可使用.ONESHELL 标签,使多行命令运行在同一个 shell 中)

面向依赖关系

写一个 Makefile 文件的第一步不是一个猛子扎进区试着写一个规则,而是先用面向依赖关系的方法想清楚,所要写的 Makefile 需要表达什么杨的依赖关系。

运用依赖关系去思考,在写 Makefile 时,头脑会非常清楚自己在写什么,以及后面要写什么。

所以,先抛开 Makefile,看一看源代码的依赖关系是什么。

规则

taget ... : prerequisites
	command
	...
	...

目标 && 伪目标

  • 目标是一个文件

  • 伪目标不是一个文件,只是一个标签

目标不一定每次都被执行,因为其依赖可能没有被修改。但是伪目标必然会被执行。

可以想像目标与文件关联,伪目标不与文件关联。

使用 .PHONY 来显式地指明一个目标是 “伪目标”。

注释

井号 # 后面的内容为注释。

注意:

  1. 如果在 Tab 后面使用 # 进行注释,虽然注释不会被执行,但会被打印。
  2. 注释行的结尾如果存在反斜线(\),那么下一行也会被作为注释行(命令行中不是)。

make 的工作方式

  1. 读入所有的 Makefile
  2. 读入被 include 的其它 Makefile
  3. 初始化文件中的变量
  4. 推导隐式规则,并分析所有规则
  5. 为所有的目标文件创建依赖关系链
  6. 根据依赖关系,决定哪些目标要重新生成
  7. 执行生成命令

1~5 步为第一个阶段,6~7 步为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make 会把其展开在使用的位置。但 make 并不会马上展开,make 使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。