跳转至

MakeFile 教程

  • Makefile 是默认 make 执行的文本
  • make 原理本质上是执行一系列 shell 指令来编译源代码,同时它增加了额外功能,比如:判断是否需要重新编译生成目标文件,或者清理编译产生的中间文件。
  • Makefile 文本格式如下
  • targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
  • prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
  • command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。
targets : prerequisites
    command

注意: 其中 command 前必须要有一个 tab,即如下语法会导致错误

test:test.c
gcc -o test test.c

执行 make,报错如下

~/code/makefile$ make
makefile:2: *** 遗漏分隔符 (null) 停止。

正确语法如下

test:test.c
    gcc -o test test.c

如果不想使用 tab 作为命令行的首字符,可以修改 .RECIPEPREFIX 变量来设置其它字符,如

.RECIPEPREFIX=<
all:
<echo $(MAKEFILE_LIST)

1 入门

1.1 一个简单的 makefile 示例

使用 make 编译 c++源码,目录结构如下

~/code/makefile$ tree
.
├── makefile
└── src
    ├── include
       ├── math.cpp
       └── math.h
    └── main.cpp

其中 makefile 文件内容如下, 第一行 main 是最终生成的目标文件,是一个可执行文件,倒数 2 行是用来清理中间文件的;

main:main.o math.o
        g++ main.o math.o -o math
main.o:src/main.cpp src/include/math.h
        g++ -c src/main.cpp -o main.o
math.o:src/include/math.cpp src/include/math.h
        g++ -c src/include/math.cpp -o math.o
clean:
        rm -rf *.o

使用 make 来构建生成目标文件,make 也可以指定 makefile 文件名, 指令如 make -f filename

~/code/makefile$ make
g++ -c src/main.cpp -o main.o
g++ -c src/include/math.cpp -o math.o
g++ main.o math.o -o math
~/code/makefile$ ls
main.o  makefile  math  math.o  src

指定 make 的执行目标,如使用 make clean 命令删除中间文件

~/code/makefile$ make clean
rm -rf *.o
~/code/makefile$ ls
makefile  math  src

2 基础

2.1 makefile 文件名

  • GNUmakefile(不建议)
  • makefile
  • Makefile (推荐)

2.2 Makefile 的工作流程

2.2.1 make 总体流程

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

2.2.2 执行目标命令流程

假设一个 makefile 文件内容如下

main:main.o math.o
        g++ main.o math.o -o math
main.o:src/main.cpp src/include/math.h
        g++ -c src/main.cpp -o main.o
math.o:src/include/math.cpp src/include/math.h
        g++ -c src/include/math.cpp -o math.o
clean:
        rm -rf *.o

在执行 make 命令时,主要做如下操作: 1. 查找目录下 makefile 文件,将第一行指定的目标作为最终生成的目标,然后去查找它的依赖文件 main.o 和 math.o,通过比较 main 和 main.o 和 math.o 修改时间顺序来决定是否需要执行后面的命令。 2. 如果目标的依赖不存在,则会去找依赖文件是否作为另一个目标,比如,main 的依赖 main.o 不存在,则查找 main.o 作为目标的那一行,即第 3 行 main.o:src/main.cpp src/include/math.h, 然后又去查找 main.o 的依赖是否存在,以此类推。 3. 当目标的依赖存在,就开始执行目标的命令,层层往上,最终所有目标都生成 4. 当 main 的依赖都存在后执行 main 的命令 g++ main.o math.o -o math, 产生 main 可执行程序

2.3 make 内建目标名

2.3.1 .PHONY 伪目标

  • 功能:不管伪目标文件是否存在,伪目标关联的命令总是被执行
  • 语法:.PHONY: target1 target2 ...
  • 示例
.PHONY:clean
clean:
    rm -rf *.o
  • 解释:伪目标的作用是,伪目标后的命令总是会被执行; 因为 makefile 规则中是否去执行一条命令是需要根据依赖的文件是否比目标文件新特征来决定的,假设上例 makefile 文件所在目录下存在 clean 文件,而目标 clean 并没有依赖,也就说明目标永远比依赖新,所以当没有指定 clean 是伪目标时,clean 将永远不会执行。

2.3.2 .DEFAULT 默认目标

  • 功能:当 makefile 中目标不存在,会进入 .DEFAULT 定义的规则中
  • 示例
.DEFAULT:
    $(error no target $@)
  • 解释:目标 test 并不存在,执行默认目标规则
$ make test
makefile:9: *** no target test。 停止。

2.3.3 .SILENT 执行命令时不打印命令到标准输出

  • 功能:当目标的命令执行时,会打印这行命令,通过 .SILENT 可以指定某些目标的命令不打印
  • 示例: 目标 all 不会打印命令,目标 tar1 会打印
.SILENT:all
all:tar1
    echo $@ command
tar1:
    echo $@ command
  • 结果
$ make
echo tar1 command
tar1 command
all command

2.3.4 .ONESHELL 命令执行时使用一个子 shell

  • 功能:默认 make 执行命令时,每一行都会用新的子 shell 去执行, .ONESHELL 能让一个子 shell 去执行
  • 示例 1:启动 .ONESHELL,可以看到 shell 进程 id 都一样
.ONESHELL:
all:
    echo line 1 shell pid =$$$$
    echo line 2 shell pid =$$$$
  • 结果
$ make
echo line 1 shell pid =$$
echo line 2 shell pid =$$
line 1 shell pid =2636380
line 2 shell pid =2636380
  • 示例 1:默认行为,不启动 .ONESHELL,可以看到 shell 进程 id 不一样
all:
    echo line 1 shell pid =$$$$
    echo line 2 shell pid =$$$$
  • 结果
$ make
echo line 1 shell pid =$$
line 1 shell pid =2638943
echo line 2 shell pid =$$
line 2 shell pid =2638944

2.4 order-only 依赖

  • 解释:
  • 正常目标和根据依赖来执行命令重建目标,而 order-only 依赖,不会通过判断依赖文件的时间来重建目标,但依赖往后的规则仍然会执行
  • 语法:
  • target:normal-prerequisites|order-only-prerequisites :可以有常规依赖和 order-only 依赖
  • target:|order-only-prerequisites : 可以不要常规依赖
  • 示例:假设 makefile 文件如下
file1:file2
    touch $@
file2:
    touch $@
  • 执行 make, 生成文件 file2,file1
make
touch file2
touch file1
  • 再删除文件 file2,再执行 make,此时 file1 也被重建了
$rm file2
$ make
touch file2
touch file1
  • 如果只想重建 file2,却不重建 file1,这时可以使用 order-only 依赖,makefile 如下
file1:|file2
    touch $@
file2:
    touch $@
  • 再次执行 make,此时只会重建 file1
$rm file2
$ make
touch file2

2.5 命令前缀符号

2.5.1 @不显示命令

假设 makefile 文件如下:

.PHONY:clean
clean:
    rm -rf *.o

当执行一条命令时,控制台会打印当前执行的命令,如下

~/code/makefile$ make clean
rm -rf *.o

而当我们给命令前增加 @ 符号时,makefile 如下:

.PHONY:clean
clean:
    @rm -rf *.o

执行 clean,命令不会打印出来

~/code/makefile$ make clean

2.5.2 -忽略命令执行错误

比如当我们执行 mkdir 创建目录时,如果已经存在目录,那么执行 mkdir 时会出错,但这并不应该导致 make 执行失败,因此 - 用来处理这类情况。

.PHONY:clean
clean:
    -rm -rf *.o

上面这条 rm 指令不管失败还是成功,并不会影响 make 继续执行

2.6 长行分离符

  • 符号:\, 类似其它编程语言,如 c 中
  • 功能:便于排版,将长行转换多行
  • 示例
var1:=hello\
  word
var2:=hello$\
   word
all:
    echo var1=$(var1)
    echo var2=$(var2)
  • 结果
$ make
var1=hello word
var2=helloword

3 进阶

3.1 include 引用其它 makefile 文件

  • 语法形式:include filenames...
  • -include : 在 include 前加上符号 - 会忽略不存在的 make 文件,而不报错
  • 示例
## 包含sub.make文件
include sub1.make sub2.make

3.2 VPATH和vpath的使用

在使用VPATH或vpath时,命令中指定的源文件名不能显示指定而需要通过自动化变量来指定,如$@$^$<等等取代真实文件名如math.cpp

3.2.1.1 VPATH

VPATH用来指定文件的搜索路径 - 语法形式:路径之间可以用:分割,也可以用空格分割 - VPATH=dir1:dir2 - VPATH=dir1 dir2
假设源文件结构层次如下

~/code/makefile$ tree
.
├── makefile
└── src
    ├── include
       ├── math.cpp
       └── math.h
    └── main.cpp

多个文件分布在不同目录下,如果不使用 VPATH 则需要编写如下的 makefile,需要给源文件指定路径

main:main.o math.o
    g++ main.o math.o -o math
main.o:src/main.cpp src/include/math.h
    g++ -c src/main.cpp -o main.o
math.o:src/include/math.cpp src/include/math.h
    g++ -c src/include/math.cpp -o math.o
clean:
    rm -rf *.o

如果使用 VPATH,执行 make 指令会先在当前目录下查,没找到就会按顺讯去指定的目录下查找文件,当我们写 makefile 文件时就可以忽略路径信息

VPATH=src:src/include
main:main.o math.o
    g++ $^ -o math
main.o:main.cpp math.h
    g++ -c $< -o main.o
math.o:math.cpp math.h
    g++ -c $< -o math.o
clean:
    rm -rf *.o
3.2.1.2 vpath
  • 语法形式
  • vpath PATTERN DIRECTORIES : 在 DIRECTORIES 下查找 PATTERN
  • vpath PATTERN :清除 PATTERN 搜索路径,使其不再使用方式 1 中定义的搜索路径
  • vpath :清除所有搜索路径,使方式 1 中定义的搜索路径失效

其中 PATTERN 指待寻找文件 (可由通配符组合),DIRECTORIES 指查找路径
假设源文件结构层次如下

~/code/makefile$ tree
.
├── makefile
└── src
    ├── include
       ├── math.cpp
       └── math.h
    └── main.cpp

使用 vpath 格式写的 makefile 内容如下

vpath math.% src/include
vpath main.cpp src
main:main.o math.o
    g++ $^ -o math
main.o:main.cpp math.h
    g++ -c $< -o main.o
math.o:math.cpp math.h
    g++ -c $< -o math.o
clean:
    rm -rf *.o

其中 vpath math.% src/include 指定以 math. 开头的文件在 src/include 目录下查找,百分号符 % 表示匹配任意字符且不限长度,vpath main.cpp src 则表明 math.cpp 文件在 src 目录下查找

3.3 条件语句

  • 语法形式
ifeq 条件
...
else
...
endif
  • 示例
libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc)
    $(CC) -o foo $(objects) $(libs_for_gcc)
else
    $(CC) -o foo $(objects) $(normal_libs)
endif

make 规则中有 4 个用条件判断的关键字

关键字 解释 语法规则
ifeq 判断是否相等 ifeq (ARG1, ARG2) or ifeq 'ARG1' 'ARG2' or ifeq "ARG1" "ARG2" or ifeq "ARG1" 'ARG2' or ifeq 'ARG1' "ARG2"
ifneq 判断是否相等 ifeq
ifdef 判断是否有值 ifdef VARIABLE-NAME
ifndef 判断是否有值 ifndef

4 扩展

4.1 makefile 书写风格

4.1.1 标准目标名称

  1. all :当需要编译整个程序时,使用 all 目标来定义规则,通常这作为默认目标。
  2. install :执行安装命令。执行完编译后,将生成的可执行文件、库、头文件等文件安装到系统下的某个位置。
  3. install-strip :和 install 目标一样作用,但会移除可执行文件的符号数据。通常 strip 过程应该将可执行文件拷贝到安装目录进行,而不是直接在 build 目录下进行。
  4. uninstall :删除 install 安装的文件。当通过 install 目标安装了某些文件时,可以通过此目标去移除这些文件。
  5. clean :删除当前目录中通常通过构建程序创建的所有文件。
  6. info :生成所需的任何 info 文件(info 文件一般是一些关于这个软件的信息,比如这个软件的用途,名字,操作系统,需要磁盘空间等信息)
  7. dist :为此程序创建 tar 文件。 tar 文件中的文件名包括其分发包的名称、版本号(可选),如 gcc1.4 版本,目录名为 gcc-1.40;tar 安装包命名必须包括其分发包的名称、版本号,且通常用 gzip 压缩,如 gcc-1.40.tar.gz
  8. check :在 build 之后,安装之前如果需要检查 build 后的程序能正常运行,可以使用此目标检测。

5 疑难问题

5.1 make 变量不能保存 shell 命令执行后输出结果

  • 解决方案:
    • 使用 export 导出环境变量,让系统环境变量存数据,而不是 makefile 里面的变量
export cur_time=$$(date "+%Y-%m-%d");sed -i "/NCDFS_RELEASE_TIME/c #define NCDFS_RELEASE_TIME \"$${cur_time}\"" common/ncsf_version.h

6 参考资料

  • https://blog.csdn.net/weixin_38391755/article/details/80380786
  • http://c.biancheng.net/view/7139.html