一、makefile文件基本介绍
`Makefile` 文件是 `make` 工具使用的配置文件,它定义了如何自动化构建项目的规则和命令。`Makefile` 文件的主要作用是指定如何编译和链接程序,以及管理文件之间的依赖关系,从而实现高效的构建过程。
1.1 `Makefile` 的基本结构
一个典型的 `Makefile` 文件由目标(target)、依赖(dependencies)和命令(commands)组成。它的基本语法如下:
target: dependencies
command
- target:要生成的文件,通常是可执行文件、目标文件、或其他文件。也可以是一个伪目标(例如 `clean`,不实际生成文件,只是执行某些命令)。
- dependencies:生成目标文件所需的文件。如果这些依赖文件有任何变化,`make` 就会执行相应的命令来更新目标。
- command:生成目标的命令。通常是编译、链接命令或其他操作系统命令。这些命令必须以 Tab 键开头。
1.2 简单的 `Makefile` 示例
假设有一个简单的 C 项目,包含以下文件:
- `main.c`
- `utils.c`
- `utils.h`
下面是一个对应的 `Makefile`:
# 编译器和编译器选项
CC = gcc
CFLAGS = -Wall -g
# 目标文件
TARGET = myprogram
# 目标及其依赖关系
$(TARGET): main.o utils.o
$(CC) $(CFLAGS) -o $(TARGET) main.o utils.o
# 规则:生成 main.o
main.o: main.c utils.h
$(CC) $(CFLAGS) -c main.c
# 规则:生成 utils.o
utils.o: utils.c utils.h
$(CC) $(CFLAGS) -c utils.c
# 清理构建生成的文件
clean:
rm -f $(TARGET) *.o
逐行解释
- CC = gcc : 变量 `CC` 定义了要使用的编译器,这里是 `gcc`。
- CFLAGS = -Wall -g : 变量 `CFLAGS` 定义了编译器选项,`-Wall` 启用所有常见的编译警告,`-g` 生成调试信息。
- TARGET = myprogram : 变量 `TARGET` 指定了最终生成的可执行文件名。
- $(TARGET): main.o utils.o : 这是生成可执行文件 `myprogram` 的规则,它依赖于 `main.o` 和 `utils.o`。如果 `main.o` 或 `utils.o` 改变,`make` 会重新链接生成 `myprogram`。
- $(CC) $(CFLAGS) -o $(TARGET) main.o utils.o**: 这是链接命令,生成最终的可执行文件。
- main.o: main.c utils.h : 生成 `main.o` 的规则,`main.o` 依赖于 `main.c` 和 `utils.h`。如果 `main.c` 或 `utils.h` 改变,`make` 会重新编译 `main.c`。
- clean : 这是一个伪目标,用于清理构建生成的文件,如可执行文件和目标文件。
1.3 使用 `Makefile`
在终端中运行 `make` 命令时,`make` 会按照 `Makefile` 中的规则,自动完成编译和链接过程。如果 `Makefile` 中定义了多个目标,可以通过 `make target` 来指定构建某个特定目标。例如:
- 默认构建: 运行 `make`,它会构建默认的目标 `myprogram`。
- 清理构建: 运行 `make clean`,它会执行 `clean` 目标,删除所有编译生成的文件。
1.4 高级特性
- 变量:`Makefile` 支持变量,用于简化和重用代码,例如 `CC` 和 `CFLAGS` 变量。
- 自动化变量:`$@` 表示目标名称,`$<` 表示第一个依赖文件,`$^` 表示所有依赖文件。这些变量可以在命令中使用。
- 条件语句:`Makefile` 支持 `if`-`else` 语句,可以根据条件设置变量或执行不同的规则。
- 模式规则:可以使用通配符来定义通用的构建规则,例如 `%.o: %.c` 可以自动匹配所有 `.c` 文件生成相应的 `.o` 文件。
通过 `Makefile`,开发者可以高效管理项目的编译和构建过程,尤其是在大型项目中,可以显著提高工作效率。
二、makefile 基本语法
2.1 基本语法示例
举例 1:
语法格式:
目标:依赖
(tab)命令
举例:
目标: all
依赖:空
命令: gcc helloworld.c -o helloworld
举例 2:
在学习 gcc 编译的时候,讲解了编译的流程,可以将上面的例子写成以下格式:
目标:all 和 helloworld.o
依赖:helloworld.o 和 helloworld.c
命令: gcc helloworld.c -o helloworld 和 gcc -c helloworld.c -o helloworld.o
因为 all 依赖 helloworld.o 文件,所以要先执行 gcc -c helloworld.c -o helloworld.o 得到 helloworld.o 文件,然后才可以执行 gcc helloworld.c -o helloworld 。所以输入 make 命令后执 行顺序如下图所示。
举例 3:
在编译的时候,可以使用“make 目标”的命令格式来进行编译,如果不进行目标的指定, 默认执行的是第一个目标所对应的规则。也就是说在上一个例子中 make 和 make all 执行结果是相同的。 修改 makefile 代码如下图所示:
保存退出之后,输入命令 “make clean” 就可以直接执行 rm -rf *o helloworld 命令。如下图所示:
但是在当前目录下不能有和 makefile 目标名一样的文件。比如在当前目录下创建一个名为 clean 的文件,然后执行make clean 命令就会报错。如下图所示。
为了解决这个问题,makefile 引入了一个新的概念,叫做伪目标,使用伪目标来声明 clean 就可以避免与当前目录下的同名文件发生冲突。
伪目标内容格式
.PHONY:目标
可以把上面的代码修改成如下图所示:
然后在执行 make clean 命令。尽管当前目录下有 clean 同名文件, make clean 命令也可以执行成功。如下图所示。
2.2、makefile变量和变量赋值
变量可以对许多地方使用,比如目标,依赖。或者命令。
变量的赋值可以使用: = ?= := +=
变量的使用:通过$() 来完成变量的引用。
示例 1:
使用 := 来赋值
使用 := 来给变量赋值,是立刻赋值,在执行 var1:=aaa 的同时 var1 变量值已经被确定了, 所以最后打印为 aaabbb,而不是 cccbbb,如下图所示。
示例 2:
使用 = 来赋值
使用 =来赋值,是延迟赋值,使用 = 来赋值是 makefile 里面最后被指定的值。因为最后给变量 var1 赋值为 ccc ,所以最后打印为 cccbbb ,而不是 aaabbb ,如下图所示:
示例 3:
使用 ?= 来赋值
使用 ?= 来赋值,如果 var1 变量前面没有被赋值,那么就给它赋值为 ccc ,如果前面已经赋值了,就适应前面的值,所以,打印为 aaabbb ,而不是 cccbbb ,如下图所示 :
然后注释掉第一行代码,makefile 中的注释为 #
在运行就会打印 cccbbb ,因为前面没有给 var1 变量赋值。如下图所示:
示例 4 :
使用 += 来赋值
使用+=赋值是追加赋值,是在前面定义的好的字符串里面在添加进去新的字符串,所以运行会打印aaa cccbbb。中间会有空格,如下图所示:
使用 += 也类似于这样赋值,如下图所示:
如果赋值很长,也可以使用换行符\,如下图所示:
2.3 自动化变量
自动化变量就是不用定义且会随着上下程序的不同而发生变化的变量叫做自动化变量。 这里介绍三个最常用的自动化变量:
$@: 表示所有目标
$< :表示第一个依赖文件,如果依赖模式是%,那么他就表示一系列文件。 (%为通配符,类似 linux 上的 *)
$^ :表示所有依赖。
在了解这三个自动化变量之前,先来建立一个工程,具体步骤如下:
首先使用命令 “vim main.c” 创建 main.c 文件,并在文件中添加以下内容,添加完成如下图所示:
然后使用命令 “vim helloworld.c” 创建 helloworld.c 文件,并在文件中添加以下内容,添加完成如下图所示:
然后使用命令 “vim helloworld.h” 创建 helloworld.h 文件,并在文件中添加以下内容,添加完成如下图所示:
这三个文件创建完成之后如下图所示:
最后使用命令“vim makefile”创建 makefile 文件,并在文件中添加以下内容,添加完成如下图所示:
最后使用make指令完成编译,如下图所示:
使用这个 makefile 虽然也可以成功编译,但是,一旦编译的文件多了,仍用这种方法来编写 makefile 就会变得非常复杂。所以,自动化变量就派上用场啦。 接下来一步一步的来简化 makefile 。
简化一:
用变量表示依赖文件
后面如果再增加依赖文件的话,直接在变量 var 后面增加就可以了。
简化二:
使用通配符 % ,和自动化变量 $< 、$@代替依赖和目标,简化完如下图所示:
简化三:
使用自动化变量 $^ 表示所有文件依赖的列表,简化完如下图所示:
2.4 makefile常用函数
2.4.1 wildcard 函数
格式: $ (wildcard PATTENR)
功能: 展开指定的目录
举例:
在/home/topeet/Desktop/test 目录有一个叫 test1.txt 文件和一个 test 的文件夹,在/home/topeet/Desktop/test/test 文件夹下有一个 test2.txt 的文件。创建过程如下三幅图所示。
在当前目录下创建的 makefile 里面写下如下代码,echo 前面加了@ 符号,echo 这个命令就不显示:
执行结果:
得到了./test1.txt 和./test/test2.txt ,所以 wildcard 函数会把指定目录下的文件展开。
2.4.2 notdir 函数
格式: $ (notdir $ (var) )
功能:去掉路径。
举例: 将上文中的 wildcard 函数替换为 notdir ,替换完成如下图所示:
执行结果:
因为 notdir 函数可以去掉路径,所以 ./test1.txt 和 ./test/test2.txt 去掉路径就得到了 test1.txt test2.txt。
2.4.3 dir 函数
格式: $(dir )
功能:取出目录,这里的目录指的是最后一个反斜杠/ 之前的部分,如果没有反斜杠/就返回当前。 举例:
在上面的例子中加入以下代码,如下图所示:
因为 var 的值为 ./test1.txt 和 ./test/test2.txt ,所以取出目录就是 ./ 和 ./test/ ,如下图所示:
2.4.4 patsubst 函数
格式: $(patsubst 原文件,目标文件,文件列表)
功能:替换文件后缀
举例:将上述例子中的 test1.txt 和 test2.txt 修改为 test1.c 和 test2.c,修改 makefile 文件中的内容,如下图所示:
这个函数会把 var1 变量的 test1.c 和 test2.c 的 .c 后缀替换为 .o ,如下图所示:
但是这个替换并不会改变当前目录下的后缀名。如下图所示。
这个函数能做什么呢?可以用这个函数来替换后缀名,进行其他的操作,这个函数都是会配合其他函数来用的。
可以使用这个函数进行替换,也可以使用 $(var:a=b) 这个格式来替换,var 代表要替换文件的名字,a 是原文件,b 是目标文件。来对上面的代码进行修改,修改完成如下图所示:
运行结果如下:
2.4.5 foreach 函数
格式:$(foreach <var>,<list>,<text>)
功能:把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。每一次 <text> 会返回一个字符串。
举例:
因为 var2 变量的值为 ./ 和 ./test ,所以先把 ./ 取出来放在 n 变量,然后再执行 wildcard 函数取出 ./test 和 ./test 下面的 c 文件的路径。所以执行结果如下图所示: