Makefile 入门:从基础到实用技巧

未分类
1.1k 词

Makefile 入门:从基础到实用技巧

一、为什么需要Makefile?

简单说,Makefile是「编译规则说明书」。一个工程可能有几十上百个源文件,按功能放在不同目录里。Makefile定义了:

  • 哪些文件先编译,哪些后编译
  • 哪些文件修改后需要重新编译
  • 甚至可以执行打包、备份等额外操作

有了Makefile,只需敲一个make命令,整个工程就会自动编译,极大提高开发效率。

二、先搞懂:编译和链接

在讲Makefile之前,先明确两个基本概念:

  • 编译(compile):把源代码(.c/.cpp)变成中间目标文件(Unix下是.o,Windows下是.obj)。编译器只检查语法和函数/变量是否声明。
  • 链接(link):把一堆中间目标文件拼成可执行文件。链接器找函数的实现,找不到就会报错(比如VC里的Link 2001)。

举个例子:main.c编译成main.otool.c编译成tool.o,最后链接成app可执行文件。

三、Makefile 基本规则

Makefile的核心是「依赖关系」和「执行命令」,格式如下:

1
2
3
目标(target)... : 依赖(prerequisites)...
命令(command)
...
  • 目标(target):可以是可执行文件、中间目标文件(.o),甚至是一个动作(如clean)。
  • 依赖(prerequisites):生成目标需要的文件或其他目标。
  • 命令(command):生成目标的具体操作(必须以Tab键开头)。

规则逻辑:如果「依赖文件」比「目标文件」新(或目标不存在),就执行命令生成目标。

四、一个简单示例

假设工程有8个.c文件和3个头文件,要编译成可执行文件edit。一个基础的Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 最终目标:edit(依赖所有.o文件)
edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

# 每个.o文件的依赖和编译命令
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
# ... 其他.o文件类似 ...

# 清理目标:删除编译产物
clean :
rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

用法:

  • make:自动编译所有需要更新的文件,生成edit
  • make clean:删除edit和所有.o文件,方便重新编译。

五、简化Makefile的技巧

上面的示例有很多重复代码(比如一堆.o文件名),可以用「变量」和「自动推导」简化。

5.1 用变量减少重复

把重复出现的内容定义成变量,比如所有.o文件:

1
2
3
4
5
6
7
8
9
10
# 定义变量objects,包含所有.o文件
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

# 使用变量$(objects)代替重复的文件名
edit : $(objects)
cc -o edit $(objects)

# 清理命令也用变量
clean :
rm edit $(objects)

以后新增.o文件,只需改objects变量即可。

5.2 自动推导(隐含规则)

GNU make很智能:看到.o文件,会自动找对应的.c文件作为依赖,并且自动生成编译命令(cc -c 源文件)。

简化后的Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)

# 只需写.o依赖的头文件,编译命令不用写
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
# ... 其他.o文件的头文件依赖 ...

# 声明clean是伪目标(避免和同名文件冲突)
.PHONY : clean
clean :
rm edit $(objects)

是不是简洁多了?

六、实用技巧

6.1 伪目标(.PHONY)

clean这种「动作型目标」,不是真实文件,最好用.PHONY声明,避免和目录中同名文件冲突:

1
2
3
.PHONY : clean  # 声明clean是伪目标
clean :
rm edit $(objects)

6.2 自动生成依赖

大型工程中,手动写.o依赖的头文件很麻烦。可以用编译器的-MM参数自动生成依赖:

1
2
3
4
5
6
7
8
9
10
# 自动生成每个.c的依赖文件(.d)
%.d: %.c
@set -e; rm -f $@; \
cc -MM $(CPPFLAGS) $< > $@.tmp; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp

# 引入所有.d文件
sources = main.c kbd.c command.c display.c insert.c search.c files.c utils.c
include $(sources:.c=.d)

这样头文件修改后,Makefile会自动识别需要重新编译的文件。

6.3 嵌套执行make

大型工程可以按模块分目录,每个目录放一个Makefile,总控Makefile调用子目录的Makefile:

1
2
3
# 编译子目录subdir
subsystem:
cd subdir && $(MAKE) # 等价于 $(MAKE) -C subdir

七、常用函数

Makefile有一些实用函数,帮你处理字符串、文件名等:

  • 字符串替换$(subst 旧字符串,新字符串,原字符串)
    例:$(subst .o,.c,main.o) → 结果是main.c

  • 取目录$(dir 文件名)
    例:$(dir src/main.c) → 结果是src/

  • 过滤文件$(filter 模式,文件列表)
    例:$(filter %.c,main.c tool.o test.c) → 结果是main.c test.c