Linux下make工具的使用

发布时间 2023-11-15 17:16:58作者: Beasts777

环境:Ubuntu 18.04.6

文章参考:爱编程的大丙 (subingwen.cn)

简介:

gcc命令可以帮助我们编译源文件,但当源文件数量多到一定程度时,使用gcc命令就会变得较为复杂。项目构建工具make应运而生,make是一个命令工具,用于解释makefile中指令的命令工具。

在构建项目时,make工具会自动加载当前目录下一个叫makefileMakefile的文件,它规定了哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,以及一些更加复杂的操作。总的来说,makefile就像是一个shell脚本一样,其中也可以执行操作系统的命令。

1. 规则

makefile的基本语法规则如下:

# 每条规则的语法格式:
target1 target2...:depend1 depend2...
	command		# 前面是tab制表符
	command
	...

总的来说,每条规则分为三部分:

  • 命令:当前这条规则需要执行的动作,一般来说就是shell命令。
    • eg:通过某个编译命令,生成库文件,进入目录等。
    • 动作可以是多个。每个动作单独占一行,且前面有一个tab制表符。
  • 依赖:规则所必须依赖的条件,在规则的命令中可以使用这些依赖。
    • 例如:汇编产生的二进制目标文件*.o就可以作为依赖使用。
    • 如果规则的命令中不需要使用到依赖,那么规则的依赖可以为空。
    • 当前规则中的依赖可以是其他规则中的目标,这样就形成了规则之间的嵌套。
    • 依赖可以根据要执行的命令的实际需求,指定很多个。
  • 目标:规则中的目标,这个目标和规则中的命令是对应的。
    • 通过执行规则中的命令,会生成一个和目标同名的文件。
    • 规则中可以有多个命令,因此可以通过多条命令来生成多个目标,所以目标可以有多个。
    • 如果通过执行规则中的命令,并不生成任何文件,那么目标被称为伪目标

EG:

测试代码结构如下:

.
├── add.c		# 加法源文件
├── include
│   └── head.h	# 头文件
├── main.c		# 测试文件
└── sub.c		# 减法文件
  • 单条规则编写:

    创建makefile文件,随后添加如下命令:

    app:add.c sub.c main.c
    	gcc add.c sub.c main.c -o app -I ./include/
    

    然后在该目录下运行make命令即可。

  • 嵌套规则编写:

    app:add.o sub.o main.c			# 这里使用的材料文件并不存在,因此下面要编写新的规则,用于生成这里的材料文件
    	gcc add.o sub.o main.c -o app -I ./include/
    add.o:add.c
    	gcc -c add.c -o add.o -I include
    sub.o:sub.c
    	gcc -c sub.c -o sub.o -I include	
    

    运行make指令后目录结构如下:

    .
    ├── add.c
    ├── add.o
    ├── app
    ├── include
    │   └── head.h
    ├── main.c
    ├── makefile
    ├── sub.c
    └── sub.o
    

2. 工作原理

本节主要介绍make执行过程中的一些原理。

2.1 规则的执行

  • 扫描解析顺序:

    make在执行规则时,首先扫描第一条规则,如果第一条规则使用的材料是下面规则的目标,那么也会扫描下一条规则。

  • 执行规则:当前规则是否执行,分为两种情况:

    • 目标不存在,那么必须要执行。
    • 目标存在,但存在材料的更新时间戳大于目标的更新时间戳的情况,或者材料不存在,那么就要重新执行该规则。
  • 自动推导:make支持对一些文件的自动推导,比如以汇编后的.o文件为原料,但实际上只存在其源文件,那么make会自动为根据同名源文件汇编出二进制文件。并且此功能也会支持文件的更新,即某个源文件更新时,所有涉及该源文件的规则都会更新。

EG:

  • 场景一:

    # makefile规则如下:
    app:add.o sub.o main.c
    	gcc add.o sub.o main.c -o app -I ./include/
    add.o:add.c
    	gcc -c add.c -o add.o -I include
    sub.o:sub.c
    	gcc -c sub.c -o sub.o -I include	
    

    当前目录结构如下:

    .
    ├── add.c
    ├── add.o
    ├── app
    ├── include
    │   └── head.h
    ├── main.c
    ├── makefile
    ├── sub.c
    └── sub.o
    

    此时目标文件、材料均存在且为最新。

    此时执行make命令:

    make
    make: “app”已是最新。
    
  • 场景二:

    仍是场景一中的文件,但将sub.o删除

    # 执行
    rm sub.o
    make
    # 输出
    gcc -c sub.c -o sub.o -I include	
    gcc add.o sub.o main.c -o app -I ./include/
    

    可以看到,此时以sub.o作为目标和原料的两句命令都被重新执行了。

  • 场景三:

    仍是场景一中的文件,但修改add.c

    # 执行
    make
    # 输出
    gcc -c add.c -o add.o -I include
    gcc add.o sub.o main.c -o app -I ./include/
    

    可以看到,以add.c为原料的规则,和以add.o为原料的规则全都执行。

  • 场景四:

    仍是场景一中的文件,但修改add.c,并修改makefile如下:

    # makefile规则如下:
    app:add.o sub.o main.c
    	gcc add.o sub.o main.c -o app -I ./include/
    sub.o:sub.c
    	gcc -c sub.c -o sub.o -I include	
    

    也就是删除了以add.o为目标,add.c为依赖的规则,随后执行make命令如下:

    # 指令
    make
    # 输出
    cc    -c -o add.o add.c
    gcc add.o sub.o main.c -o app -I ./include/
    

    可以看到,即使没有以add.c为依赖的规则,且存在现成的add.o文件,make工具依旧会在add.c源文件更新时执行自动推导,生成新的add.o文件,并随之执行以add.o为依赖的规则。

3. 变量

在使用makefile时,为了使文件变得更加灵活,make提供了变量支持,共有三种变量:自定义变量预定义变量自动变量

3.1 自定义变量

用户自己定义的变量就是自定义变量。需要注意的是,特点如下:

  • 没有类型。
  • 创建时必须赋值。
  • 使用时使用$(变量名)进行取值。

EG:

执行:

obj = add.o sub.o main.c
target = app
$(target):$(obj)
        gcc $(obj) -o $(target) -I include

输出:

cc    -c -o add.o add.c
cc    -c -o sub.o sub.c
gcc add.o sub.o main.c -o app -I include

3.2 预定义变量

所谓预定义变量,就是makefile预先定义好的变量,用户直接拿来使用就可以了,这些预定义变量都是全大写形式。

特点:

  • 预定义变量使用时也需要通过$()进行取值。

一些常用预定义变量:

变量名 含义 默认值
AR 生态静态库文件的程序名 ar
AS 汇编编译器名称 as
CC c语言编译器名字 cc
CPP c语言预编译器名字 $(CC)-E
CXX c++语言编译器名字 g++
FC FORTRAN语言编译器的名称 f77
RM 删除文件的程序名 rm -f
ARFLAGS 生成静态库库文件程序的名称
ASFLAGS 汇编语言编译器的选项
CFLAGS C语言编译器的编译选项
CPPFLAGS C语言预编译的编译选项
CXXFLAGS C++语言编译器的编译选项
FLAGS FORTRAN语言编译器的编译选项

EG:

对上一小节的代码进行替换:

obj = add.o sub.o main.c
target = app
$(target):$(obj)
        $(CC) $(obj) -o $(target) $(CFLAGS) -I include

3.3 自动变量

所谓自动变量,其实就是用来代替目标文件和依赖文件的变量,通过它们可以获取到当前规则中目标文件和依赖文件的值。

特点如下:

  • 自动变量只能在规则的命令中使用,不能用在依赖项和目标项中。
  • 使用时无需使用$()取值,直接使用即可。

一些常用自动便量:

变量 含义
$* 目标文件的名称,但不包含后缀名
$+ 表示所有的依赖文件,以空格分隔。但不会去重
$< 表示第一个依赖文件
$? 依赖项中,所有时间戳比目标项晚的依赖文件。
$@ 目标文件的名称,包含后缀名
$^ 所有依赖文件,以空格分隔。会进行去重。

EG:

obj = add.o sub.o main.c
target = app
$(target):$(obj)
        $(CC) $^ -o $@ $(CFLAGS) -I include

4. 模式匹配

所谓模式匹配,就是将一些规则高度重合的特点抽象出来,通过模式匹配,将这多个规则抽象为一个规则,以减少操作。

一个模式匹配前的例子:

obj=add.o sub.o main.c
target=app
$(target):$(obj)
        gcc $^ -o $@ -I include
add.o:add.c
        gcc add.c -c
sub.o:sub.c
        gcc sub.c -c

可以看到:后两条规则实际上除了文件名不一致,其余都一致,这里就可以使用模式匹配。通过makefile中提供的%来对文件名进行匹配。

obj=add.o sub.o main.c
target=app
$(target):$(obj)
	gcc $^ -o $@ -I include
# 此处通过%来匹配文件名
%.o:%.c
	gcc $< -c	# 通过$<匹配依赖项中的第一个

5. 函数

makefile中提供了一些函数用于简化makefile文件的编写,其特点如下:

  • 所有函数均有返回值。
  • 调用时形式为:$(函数名 参数一 参数二 ...)。不同参数之间通过空格进行区分。

下面是两个常用的函数:

5.1 wildcard函数

作用:

用于获取指定目录下指定类型的文件名,返回值通过空格进行分隔,函数原型如下:

$(wildcard PATRTTEN1 PARTTEN2 ...)
  • 参数:

    • PARTTEN指的是某个目录下匹配的文件名,例如*.c~/coding/c_c++/cal/*.o

    • PARTTEN可以有多个,从而指定多个目录下的多个类型的文件,参数之间用空格间隔。

  • 返回值:返回指定目录下匹配的所有文件的名字。

EG:

# 返回当前目录和~/coding/c_c++/test/下的所有c语言源文件名
dep=$(wildcard *.c ~/coding/c_c++/test/*.c)

5.2 patsubst

作用:按照指定的模式替换指定文件名的后缀。

函数原型:

$(patsubst pattern,replacement,text)
  • 参数:共有三个,参数间以逗号间隔。
    • pattern:一个模式字符串,指出被替换的文件的后缀名,因为不关心名字,所以通常使用%来替换名字,同时也要指出后缀名,如%.c
    • replacement:也是模式字符串,指出替换后的文件后缀,同样使用%来替换名字,同时也要指出替换后的后缀名,如%.o
    • text:需要进行操作的文件名。
  • 返回值:被替换后的文件名。

EG:

dep=add.c sub.c
dep1=$(patsubst %.c,%.o,$(dep))

6. 伪目标

有时在makefile中,某条规则执行并不产生目标,这时该规则的目标就是伪目标,

EG:

src=$(wildcard *.c)
obj=$(patsubst %.c,%.o,$(src))
target=app
$(target):$(obj)
        gcc $^ -o $@ -I include
%.o:%.c
        gcc $< -c
# 这一句并不会生成目标文件,因此clean是伪目标
clean:
        rm $(obj) $(target)

此时如果想要单独执行clean规则,那么只需:

# 输入
make clean
# 输出
rm add.o sub.o main.c app

问题:

如果当前目录下存在一个名为clean的文件,那么由于该规则没有依赖,所以在执行该规则前比较目标和依赖的时间戳时,目标总是最新的,这会导致该条规则永远无法执行。因此需要一个办法,免去规则执行前的时间戳比较。

方法:

在makefile文件中声明clean是一个伪目标即可,声明方法为:

.PHONY:伪目标名称

修改:

src=$(wildcard *.c)
obj=$(patsubst %.c,%.o,$(src))
target=app
$(target):$(obj)
        gcc $^ -o $@ -I include
%.o:%.c
        gcc $< -c
.PHONY:clean	#声明clean是伪目标
clean:
        rm $(obj) $(target)

7. 案例

一个案例,结构如下:

.
├── add.c		# 加法文件
├── include		# 头文件所在目录
│   └── head.h	# 头文件
├── main.c		# 测试程序
└── src			# 减法文件所在目录
    └── sub.c	# 减法文件

编写对应的makefile文件如下:

# 指定目录下所有的C语言文件,用*匹配
src=$(wildcard *.c ./src/*.c)
# 将src中所有文件名后追替换为.o
dep=$(patsubst %.c,%.o,$(src))
# 目标文件
target=app
# 头文件所在目录
include=./include

# 第一条规则
$(target):$(dep)
	gcc $^ -o $@

# 第二条规则,进行模式匹配
%.o:%.c
	gcc $< -c -I $(include) -o $@

# 声明伪目标
.PHONY:clean
# 清楚本makefile生成的所有文件
clean:
	rm $(target) $(dep) -f