MakeFile

MakeFile

一、基础语法

1. 基本概念

1.1 make是什么

当一个项目中要编译的文件很多时,手工使用编译器一个个进行编译,很明显不具有可操作性,此时必须借助某些软件,协助我们有序地、正确地自动编译整个工程的所有该编译的文件。这样的软件被称为 工程管理器make 就是一款工程管理器软件。

1.2 Makefile是什么

make 正常工作时,会读取一个称为 Makefile 的配置文件,该配置文件可以为 make 指明细致的工作规则,比如所使用的工具链、要编译的目标文件名称、要递归编译的子文件夹路径等等。
对工程管理器软件的学习,主要就是对其配置文件** Makefile 的语法的学习**。

1.3 Makefile在哪里

Makefile 是用来指导make对源代码进行编译的,因此在一个多目录结构的工程项目中,凡是有源码出现的目录,都会有一个Makefile去管理,而所有的 Makefile,都通过工程项目顶层目录下的 Makefile 去直接或简洁调用。

2. 目标与依赖

目标和依赖是 Makefile 语法中最基本的概念,假设有一个源文件 a.c,编译生成 a.o ,那么前者是依赖,后者 a.o 是目标,但进一步将a.o编译成可执行文件 a,那么 a.o 此时就变成依赖,最终的文件 a 是目标,因此目标和依赖是相对的概念。

关系图

Makefile中,使用冒号来区隔它们:

# 目标:依赖
a.o:a.c

# 目标:依赖列表
image:a.o b.o c.o d.o

示例:

main:main.c
    @echo "Hello MakeFile"

解析:

  • main:main.c main为目标他依赖于main.c
  • 第二行中开头必须是一个制表符不能是空格
  • @ 表示执行该语句但不需要输出他
  • echo 实际上是shell命令的“打印函数”
  • 以上示例中的两行就是一套完整的规则

3. 规则

在目标与依赖下面,使用一种特殊的语法 "语句" 来构成一个规则,比如:

# 一套规则:
a.o:a.c 
    gcc a.c -o a.o -c -fPIC  # 行首必须是制表符tab

请注意:在上述语句中,目标与依赖、命令共同构成了一个规则,命令的行首必须是制表符 tab 键,不能是空格,否则会报错。另外,命令可以是多行:

image:a.o b.o c.o d.o 
    gcc a.c -o a.o -c -fPIC
    gcc b.c -o b.o -c -fPIC
    gcc c.c -o c.o -c -fPIC
    gcc d.c -o d.o -c -fPIC
    gcc a.o b.o c.o d.o -o image

重点:规则中的各个命令什么时候被执行?

  • 当目标文件不存在时。
  • 当目标文件存在,但时间戳比依赖列表中的某一文件旧时

因此,当目标文件已经被编译且其依赖文件没有修改,那么再次执行make就不会触发任何动作(make: 'main' is up to date.),这就是make和 Makefile 的最基本的逻辑:只在有需要的时候编译,尽量提高编译效率。

4. 终极目标

在一个 Makefile 中,可以有多套规则,也就说可以有多个目标,在这多个目标中,最先出现的被称为终极目标,它是执行make时默认的目标,比如:

a:a.c
    gcc a.c -o a

b:b.c
    gcc b.c -o b

以上Makefile中,a是终极目标,b不是,因此直接执行make时,只会针对第一套规则进行推导:

gec@ubuntu:~$ ls
a.c b.c Makefile
gec@ubuntu:~$ make
gcc a.c -o a

要执行第二套规则,则需要在执行make命令时特意指定,比如:
make 指定的目标

gec@ubuntu:~$ make b
gcc b.c -o b

或令其间接依赖于终极目标,比如:

a:a.c b
    gcc a.c -o a

b:b.c
    gcc b.c -o b

执行结果是:

gec@ubuntu:~$ make
gcc b.c -o b
gcc a.c -o a

5. 多目标编译

从上述第4点可见, Makefile 中可以通过目标的相互依赖来递推整条编译链,当然像上述那样将a强行依赖于b并不是一种可取的做法,因为这么做虽然可以达到目的,但在逻辑上却让人陷入困惑,毕竟在上述例子中,a和b是两个不相干的程序,他们之间并没有依赖关系。
对于这种多目标编译,更传统的做法是,虚构一个被大家共同依赖的伪目标,利用 Makefile 编译链自动编译所有的目标,比如:

all:a b

a:a.c
    gcc a.c -o a

b:b.c
    gcc b.c -o b

执行结果是:

gec@ubuntu:~$ make
gcc a.c -o a
gcc b.c -o b

示例:

# Makefile默认第一个规则中的目标为最终目标
main:main.o test.o
    gcc main.o test.o -o main

main.o:main.c
    gcc main.c -o main.o -c 

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

6. 隐式规则

Makefile 会根据目标和依赖简单地自动推导出编译语句,这种情况叫隐式规则,比如:

all:a b

在上述 Makefile 中,没有任何编译语句,甚至连a和b的依赖文件都没写,但这个 Makefile 可以正常执行:

gec@ubuntu:~$ make
cc a.c -o a
cc b.c -o b

此时,Makefile 的执行逻辑是:监测到终极目标的依赖文件a和b不存在,就会自动寻找以a和b为目标的规则,在本文件中没有,然后就会尝试在本目录中寻找a.cb.c ,如果找到了就以它们为依赖文件,自动编译它们,这个过程就是隐式规则。
注意到,隐式规则可以帮忙处理一些比较简单地编译,它要求目标文件和依赖文件同名(除了后缀不同),不支持多文件编译,也不支持个性化编译选项。

7. 伪目标

由于有隐式规则的存在,因此伪目标在某些极端情况下可能会被误编译,比如上述例子中,all 是伪目标,不是真正要编译生成的目标,但如果源码目录中恰巧有一个文件叫 all.c ,那么根据 Makefile 的隐式规则,将会触发 all.c 的编译动作。
如何规避隐式规则这种误操作呢?很简单,明确告诉Makefileall是伪目标,不要编译他:

all:a b

.PHONY:all

上述语句中,.PHONY Makefile 的一个关键字,用来声明伪目标,防止隐式规则滥用。
在 Makefile 中,常见的伪目标除了all之外,还有cleandistclean等,用来清除生成的中间文件,例如:

all:a b

clean:  # 清除所有目标文件、可重定位文件
    rm a b *.o

distclean:clean  # 先执行clean,然后清除所有交换文件、核心转储文件
    rm .*.sw? core

.PHONY:all clean

二、变量

1. 自定义变量

类似于shell脚本,可以在 Makefile 定义变量和引用变量:

BIN=a b

# $(  )  引用变量,把需要访问的变量写入到括号里
all:$(BIN)

clean:
    rm $(BIN)

2. 内置变量

Makefile有许多跟编译相关的内置变量,比如:

CFLAGS  = "-O2 -Wall" # C编译选项
LDFLAGS = "-lpthread" # 链接器参数
CC  = aarch-linux-gnu-gcc # C编译器名称
CXX = aarch-linux-gnu-g++ # C编译器名称

可以通过修改上述变量的值,来个性化各种编译场景,例如:

CC  = gcc
CXX = aarch64-linux-gnu-g++

CFLAGS  = -O2 -Wall
LDFLAGS = -lpthread

ELF = a b

all:a b

a:a.c
    $(CC) a.c -o a $(CFLAGS) $(LDFLAGS)

b:b.cpp
    $(CXX) b.cpp -o b

clean:
    rm a b

.PHONY:all

3. 变量的定义引用

所谓定义引用,指的是在定义一个变量的时候引用了另一个变量的值。比如下面定义变量B的时候,其值引用了变量A:

A = China
B = I love $(A)

all:
    echo $(B)

执行结果:

gec@ubuntu:~$ make
echo I love China
I love China

3.1 全文搜索模式

注意到,上述变量A和B的定义,可以任意调换其顺序,比如:

B = I love $(A) # 照样可以引用出现在后面的变量A的值

all:
    echo $(B)

A = China

这不会有任何影响,这是因为 Makefile 中直接用等号 “=” 定义变量时若存在对其他变量的引用,会采取全文搜索的策略去找引用值。

3.2 简单定义模式

如果不想要 Makefile 的这种全文搜索的特性,而希望只引用定义语句之前出现过的变量的值的话,就要用 “:=” 简单模式,例如:

B := I love $(A) # 只引用在此之前有定义的A的值

all:
    echo $(B)    # 输出"I love"

A = China

3.3 变量追加

Makefile 中的变量都是字符串,可以使用 “+=” 进行追加,例如:

CFLAGS  = -O2    # 全文搜索模式
CFLAGS += -Wall
CFLAGS += -Werror

# 等价于
CFLAGS = -O2 -Wall -Werror

3.4 变量值修改

Makefile 中的变量本质是一连串单词,通常是待处理的一系列文件名称,在实际操作中经常需要对这些文件名进行模式替换,比如有一串由C语言源文件组成的字串,希望将其中的文件后缀 *.c 变成 *.o,可以这么做:

A = srt.c string.c tcl.c
B = $(A:%.c=%.o)  # 此处,变量B的值是 srt.o string.o tcl.o

4. override

在执行make时,通常可以在命令行中携带一个变量的定义,如果这个变量跟Makefile中出现的某一变量重名,那么命令行变量的定义将会覆盖Makefile中的变量。就是说,对于一个在Makefile中使用常规方式(使用“=”、“:=”或者“define”)定义的变量,我们可以在执行make时通过命令行方式重新指定这个变量的值,命令行指定的值将替代出现在Makefile中此变量的值。比如:

A = an apple tree
all:
    @echo $(A)  # 输出变量A的值,符号@代表不输出命令本身

直接执行 make 的结果是:

gec@ubuntu:~$  make A="an elephant"
an elephant

可见,虽然Makefile定义了A的值为”an apple tree”,但被命令行定义的A的值覆盖了,变成了”an elephant”。如果不想被覆盖,则可以写成:

override A = an apple tree
all:
    @echo $(A)

此时,执行结果是:

gec@ubuntu:~$  make A="an elephant"
an apple tree

但是请注意:指示符 override 并不是用来防止Makefile的内部变量被命令行参数覆盖的,其真正存在的目的是:

  • 为了使用户可以改变或者追加那些使用make的命令行指定的变量的定义。
  • 即:实现了在Makefile中增加或者修改命令行参数的一种机制。

通常,我们会通过命令行来指定一些附加的、个性化的编译参数,而对一些通用的参数或者必需的编译参数,我们则在Makefile中指定,为了使两个地方指定的参数和谐相处,不相互覆盖,一般就用指示符 override 来实现。

例如,无论命令行指定那些编译参数,必须打开所有的编译警告信息“-Wall”,则 Makefile 的变量 CFLAGS 应该这样写:

override CFLAGS += -Wall
test:test.c

执行结果是:

gec@ubuntu:~$ make CFLAGS="-g"
cc -g -Wall a.c -o a

5. 静态规则与自动化变量

所谓静态规则,就是可以使用模式匹配的方式,自动生成若干规则的机制。例如:

OBJ = a.o b.o c.o

image:$(OBJ)
    $(CC) $(OBJ) -o image

#静态规则
$(OBJ):%.o:%.c  
    $(CC) $^ -o $@ -c # 运用了自动化变量自适应不同的目标和依赖

clean:
    $(RM) $(OBJ) image

.PHONY: clean

所谓自动化变量,指的是它们的值会随着规则自动地发生变化,它们的含义是确定的,但是它们的值会自适应不同的规则,这个特性刚好与静态规则自动产生规则像。除了上面两个常见的自动化变量外,还有下述这些自动化变量。

变量 含义 备注
@ 其所在规则的目标的完整名称 -
% 其所在规则的静态库文件的一个成员名 -
< 其所在规则的依赖列表的第一个文件的完整名称 -
? 所有时间戳比目标文件新的依赖文件列表 用空格隔开
^ 其所在规则的依赖列表 同一文件不可重复
+ 其所在规则的依赖列表 同一文件可重复,主要用在程序链接时,库的交叉引用场合

三、Makefile函数

Makefile 中的函数可以实现一些特性的功能,其基本语法是:

VAR = $(函数 参数1[,参数2,参数3,...])

语法要点有:

  • 函数及其参数用 $() 包含
  • 函数与参数之间用空格隔开
  • 若函数需要多个参数,则参数之间用逗号隔开
  • 若函数有返回值,其值可以直接赋值给变量

$(subst FROM,TO,TEXT)
功能:将字符串TEXT中的字符FROM替换为TO。
返回:替换之后的新字符串。
范例:

A = $(subst pp,PP,apple tree)

替换之后变量A的值是"aPPle tree"


$(wildcard PATTERN)
功能:获取匹配模式为PATTERN的文件名。
返回:匹配模式为PATTERN的文件名。
范例:

A = $(wildcard *.c)

假设当前路径下有两个.c文件a.cb.c,则处理后A的值为:“a.c b.c


四、其他语法

嵌套Makefile

在多目录结构中,Makefile可以通过内置命令嵌套调用。例如有如下目录结构:

gec@ubuntu:~$ tree
.                   
├── dir/
│   └── Makefile   # 子Makefile
└── Makefile       # 顶层Makefile

要在顶层 Makefile 中调用子 Makefile ,只需执行如下语句:

all:
    $(MAKE) -C dir/  # 调用指定目录下的子Makefile
关系图

变量导出

在嵌套调用子 Makefile 的过程中,如果需要将变量传递给子 Makefile ,可以使用如下语句:

# 顶层`Makefile`
export A = apple    # 在顶层Makefile中,将变量A导出
B = banana          # 在顶层Makefile中,变量B未导出

all:
    @echo "rank 1: $(A)"
    @echo "rank 1: $(B)"
    @$(MAKE) -C dir/ # 调用位于dir/中的子Makefile
all:
    @echo "rank 2: $(A)" # 从顶层Makefile获得变量的值
    @echo "rank 2: $(B)" # 空值
posted @ 2025-11-15 13:25  林明杰  阅读(1)  评论(0)    收藏  举报