make:是一个命令行工具它根据一个名为Makefile的脚本文件中的指令,自动地构建和管理项目Makefile:是一个文本文件,它定义了源文件之间的依赖关系以及构建这些文件的命令
Make是一个构建工具(build tool),最早在Unix上诞生,用来自动化编译和构建项目
它的核心思想是:
- 目标(target):想要创建的东西(比如
main.o或者app.exe或者一个自定义动作如clean) - 依赖(dependencies):生成目标需要依赖哪些文件(比如源文件
.c、头文件.h)。如果任何一个依赖文件比目标文件新,make就知道目标过期了,需要重新构建 - 规则(rule):告诉Make如何从依赖生成目标(通常是编译命令)
Make根据文件的修改时间,自动决定需要重新编译哪些文件,从而避免全量编译
存在意义
想象一个由多个文件组成的C++项目:
main.cpputils.cpphelper.cppmyapp(最终的可执行文件)
没有make,每次修改后都需要手动输入一长串命令来编译
g++ -c main.cpp -o main.o
g++ -c utils.cpp -o utils.o
g++ -c helper.cpp -o helper.o
g++ main.o utils.o helper.o -o myapp
繁琐且低效。如果只修改了helper.cpp,重新编译所有文件会浪费大量时间
有了Makefile,只需要输入make,make工具就会:
- 读取
Makefile - 检查依赖关系和时间戳
- 只重新编译哪些被修改的文件以及依赖于这些文件的目标
- 最后链接可执行文件
这极大地提高了开发效率,实现了增量编译
在C/C++项目中,头文件依赖是最大的麻烦:修改了一个头文件,所有包含它的.c都要重新编译
常见的做法是让编译器生成依赖
g++ -MM main.cpp
这会输出类似:
main.o: main.cpp helper.h utils.h
通常会配合Makefile里的-include来自动引入依赖文件(如.d文件),保证依赖关系正确,而不用手写
Make的核心机制与哲学
- 依赖检查:Make会比较目标文件和依赖文件的时间戳
- 如果目标比依赖新 -> 不需要重新生成
- 如果依赖比目标新 -> 需要重新执行命令
- 声明式:在Makefile中,你声明目标和依赖,至于如何决定是否执行,由
make根据时间戳和规则自动推导。这和命令式脚本不同,后者是需要一步一步写明要做什么 - 递归构建:Make会递归地检查依赖关系
- 最小重建:只编译修改过的部分(增量编译),节省时间
- 自动化 + 增量化:它的核心价值不在于“能编译”,而在于“避免重复劳动”,只编译需要更新的部分
Makefile语法
target: dependencies
<TAB> command
target:目标文件,最终要生成的东西dependencies:依赖文件,如果依赖比目标新,Make就会执行规则command:生成目标的命令,一行或多行shell命令(注意必须以Tab开头,不能用空格代替,这是make的历史遗留语法,但必须遵守)
示例
假设有一个项目:main.c和helper.c,最终形成可执行文件hello
Makefile如下
# 最终目标:生成可执行文件 hello
hello: main.o helper.o
gcc main.o helper.o -o hello
# 目标文件 main.o 依赖于源文件 main.c和头文件 helper.h
main.o: main.c helper.h
gcc -c main.c -o main.o
# 目标文件 helper.o 依赖于源文件 helper.c和头文件 helper.h
helper.o: helper.c helper.h
gcc -c helper.c -o helper.o
# 自定义目标:清理编译生成的文件
clean:
rm -f *.o hello
# 告诉 make: 'clean' 不是一个文件,而是一个动作名称
.PHONY: clean
- 当运行
make(不加参数)时,make会默认执行Makefile中的第一个目标(这里是hello) - 为了构建
hello,它需要main.o和helper.o。如果这些.o文件不存在或比hello新,make会先去执行生成它们的规则 - 在生成
main.o时,它会检查main.c和helper.h是否比main.o新。如果只修改了helper.h,那么main.o和helper.o(因为它们都依赖helper.h)都会被重新编译,最重新链接hello。这就是依赖关系的强大之处 clean目标没有依赖。它用于删除所有编译生成的文件。可以通过make clean来执行.PHONY: clean告诉make,clean是一个“伪目标”,并不是要生成一个名为clean的文件。这是一个好习惯,可以避免如果当前目录下恰好有一个叫clean的文件时,make clean命令失效的问题
伪目标(phony targets)
.PHONY不只是clean,可以用它组织构建过程
.PHONY: all debug release
all: debug
debug:
$(MAKE) CFLAGS="-g -Wall"
release:
$(MAKE) CFLAGS="-O2 -DNDEBUG"
这体现了Make还能作为一个任务调度器,不只是编译器的前端
条件语句
ifeq ($(DEBUG),1)
CFLAGS += -g
else
CFLAGS += -O2
endif
这样就可以make DEBUG=1控制编译模式
递归Make
在大型项目里,通常每个子目录都有自己的Makefile,顶层Makefile统一调度
SUBDIRS = src utils tests
all:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir; done
变量和隐含规则
变量
可以定义变量来保存编译器名称、编译选项等
# 定义变量
CC = gcc
CFLAGS = -Wall -g
TARGET = hello
OBJS = main.o helper.o
# 使用变量 $(VAR_NAME)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
main.o: main.c helper.h
$(CC) $(CFLAGS) -c main.c
helper.o: helper.c helper.h
$(CC) $(CFLAGS) -c helper.c
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
自动变量make提供了一些特殊的“自动变量”,在规则的命令中非常有用
$@:当前规则中的目标文件名$<:第一个依赖项的文件名$^:所有依赖项的文件列表
使用自动变量可以进一步简化
CC = gcc
CFLAGS = -Wall -g
TARGET = hello
OBJS = main.o helper.o
$(TARGET): $(OBJS)
$(CC) $^ -o $@
# 使用模式规则:如何从 .c 文件构建 .o 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
这里的%.o: %.c是一个模式规则,它告诉make:任何.o文件都依赖于同名的.c文件,并使用下面的命令来构建。这使得Makefile非常简洁,无需为每个.o文件写重复的规则
GNU Make内建了大量的默认规则,比如
%.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
很多时候,甚至不用写.o的生成规则,Make会自动推导出来。这就是为什么很多简单的Makefile看起来“缺失了一些规则,但依旧能工作
make的使用
在终端中,进入包含Makefile的目录
- 最基本的用法 在项目目录下直接运行
make
默认会执行Makefile中的第一个目标
如果第一个目标是all,那就会先编译所有程序
- 指定目标
make target_name
例如
make main
make clean
那就会执行main或clean目标对应的命令
- 指定Makefile文件
默认情况下,
make会去找
GNUmakefilemakefileMakefile如果文件名不是这些,可以手动指定
make -f MyMakefile
- 多线程编译(加速)
make -j
-j:让make并行执行任务(依赖允许的情况下)- 可以指定线程数
make -j4 # 开 4 个线程
make -j8 # 开 8 个线程
对大型C++项目编译速度提升非常大
- 查看执行的命令
有时候想知道
make实际执行了哪些命令,可以加
make VERBOSE=1
make -n # 只打印命令,不执行
- 强制重新编译 有时候源文件没变,但想强制重新执行
make -B
忽略时间戳,强制执行所有命令
- 清理构建
一般
Makefile里会有
.PHONY: clean
clean:
rm -f *.o main
使用:
make clean
删除中间文件,保证下次编译干净
- 指定变量 在命令行传递变量覆盖Makefile内定义的
make CC=gcc
make CFLAGS="-02 -Wall"
非常适合调试或切换编译器
- 只执行某一步
有时候只想编译某个
.o文件
make main.o
它会自动执行生成main.o的规则,不会编译整个项目