什么是 Makefile
Makefile 简单来说就是构建某个文件需要的步骤,比如我们要编译一个 C 语言的程序,单个文件时我们可以简单输入一行 gcc 的编译命令:
$ gcc main.c -o main
就可以完成一个编译单文件的操作,但是如果项目比较复杂,涉及到多文件的编译和链接,要输入的命令就比较多,例如:
$ gcc -c module1.c module2.c main.c
$ gcc module1.o module2.o main.o -o main
这还是最简单的多文件编译,当涉及到更复杂的编译链接过程和编译器设置时,每次都敲这么多命令显然是不能接受的。
通过 Makefile,我们可以将编译命令简化为:
$ make main
这个 main
实际是自己取的名字,通过编写 makefile 的规则,可以支持多个 target 的构建,例如 make windows
构建 windows 下的程序,make linux
构建 linux 下的程序,make test
构建测试程序等等。
总之 Makefile 就是简化我们敲命令的过程。
Makefile 概念
Makefile 实际上就是一个描述命令执行过程的文本文件,就是某种意义上的 shell 脚本,由几个部分构成:
<target> : <prerequisites>
<commands>
target
就是要完成命令的名称,例如我们可以简化最简单的单文件编译命令,因为这里非常简单,没有必要设置前置条件,所以 prerequisites
可以为空:
main:
gcc main.c -o main
我们在当前目录下建立一个名叫 makefile
的文本文件,输入以上内容,随后执行 make main
即可完成编译。
文件名并不一定要是 makefile
,但是约定俗成是这样的。
target
每个 target
就是一个可执行的目标,也就是 make <target>
命令后面所跟的名字,一般来说他通常和文件名一样,构建 main
,那么最终的输出文件就是 main
。但也可以是一个操作命令,例如经常都会有的 target 就是 clean,通常用 make clean
完成项目目录临时文件的删除。
clean:
rm *.o
这样的 target
执行之后就是删除当前目录下后缀为 .o
的临时文件。
prerequisites
前置条件代表的是一种依赖关系,例如在单文件编译中,虽然命令只有一个 -o
但实际上还有许多过程,我们假设先将 main.c
编译成程序 main.o
,最后再链接成可执行程序 main
。
main: main.o
gcc main.o -o main
main.o:
gcc -c main.c
构建 main
需要 main.o
先构建完,所以会先构建 main.o
,再构建 main
,可以有多个前置条件,用空格隔开。当存在多个前置条件时,是按从前往后的顺序构建的。
变量
makefile 中可以定义自己的变量,以实现更方便的编写,变量名通常用全大写来表示,例如:
SRC_DIR := ./src
INC_DIR := ./include
makefile 中变量定义的 =
和 :=
是有一定区别的,:=
的值在赋值时就会确定,而 =
是在使用时才确定。只有在非常复杂的构建流程中才会使用到这种区分,目前只需要了解即可。
除了自定义的变量外,makefile 还有一些自动变量:
# $@ 表示构建目标
main:
echo $@ # $@ = main
# $< 表示依次获得的依赖项
main: module1.o module2.o
echo $< # $< = module1.o
main: module1.o module2.o
echo $< $< # $< = module1.o module2.o
# $^ 表示所有依赖项
main: module1.o module2.o
echo $^ # $< = module1.o module2.o
除此之外还有一些自动变量,本文就不涉及了。
通配符
在 makefile 中 %
表示通配符,匹配任意字符串,除了下一小节常用命令提到的用法,还可以作为 makefile 构建过程展开。
例如:
test_%:
echo $@
当执行 make test_anything
时,就会打印出 echo test_anything
,这里 anything
可以是任何字符串,虽然没有显式规定该目标的构建过程,但是 makefile 会自动匹配并展开。
这个也适用于是依赖项的时候,例如:
main: a.o b.o
# 编译命令
# a.o 和 b.o 的展开模板
%.o:
echo $@
这样也不用显式规定 a.o
和 b.o
的构建规则,在遇到时会自动展开,这在大量类似构建过程时,编写一个展开模板就可以省去很多代码量。
常用命令
在 makefile 中,我们可以使用以下方法来获取某个目录下的文件,并转化为某种其他形式:
假设 ./src
目录下有 module1.c
和 module2.c
SRC_DIR := ./src
INC_DIR := ./include
# 获取 src 目录下的所有 .c 文件
SRCS := $(wildcard $(SRC_DIR)/*.c)
# 将 src 目录下的所有 .c 文件名改为 .o
OBJS := $(patsubst $(SRC_DIR)/%.c, %.o, $(SRCS))
# 打印查看变量信息
$(info SRCS: $(SRCS)) # SRCS: ./src/module1.c ./src/module2.c
$(info OBJS: $(OBJS)) # OBJS: module1.o module2.o
wildcard
用于展开通配符,通常用于匹配文件名模式,其形式为:$(wildcard pattern)
。在本例中,$(SRC_DIR)
实际上是使用变量,可以等价为 $(wildcard ./src/*.c)
,也就是找下 src 目录下的所有 .c
文件。
patsubst
用于进行模式替换,其形式为:$(patsubst pattern, replacement, text)
。在本例中,将变量和通配符完全展开可以等价为:
$(patsubst ./src/add.cpp ./src/sub.cpp, add.o sub.o, ./src/add.cpp ./src/sub.cpp)
其中 %
是 makefile 中的通配符,在本例中则匹配 $(SRCS)
中满足 ./src/%.c
的文本,并替换为 %.o
,其中 %
在本例中就会被匹配为 module1
和 module2
。
常用模板:同目录
目录结构如下:
.
├── add.h
├── sub.h
├── main.c
├── makefile
├── add.c
└── sub.c
同目录下的编译较为简单,可以使用以下模板:
# 定义变量
SRCS := add.c sub.c
OBJS := $(patsubst %.c, %.o, $(SRCS))
TARGET := main
# 打印查看变量信息
$(info SRCS: $(SRCS))
$(info OBJS: $(OBJS))
# 构建目标
$(TARGET): $(OBJS)
gcc $(TARGET).c $(OBJS) -o $(TARGET)
# 编译规则
%.o: %.c
gcc -c $< -o $@
# 清理规则
clean:
rm -f $(OBJS) $(TARGET)
常用模板:项目目录
.
├── include
│ ├── add.h
│ └── sub.h
├── main.cpp
├── makefile
└── src
├── add.cpp
└── sub.cpp
项目目录下的编译复杂一些,但是只是多个了自动获取文件的步骤,可以使用以下模板:
# 定义变量
SRC_DIR := ./src
INC_DIR := ./include
TARGET := main
# 自动获取目录下的源文件和编译目标文件
SRCS := $(wildcard $(SRC_DIR)/*.cpp)
OBJS := $(patsubst $(SRC_DIR)/%.cpp, %.o, $(SRCS))
# 打印查看变量信息
$(info SRCS: $(SRCS))
$(info OBJS: $(OBJS))
# 构建目标
$(TARGET): $(OBJS)
g++ -I$(INC_DIR) $(OBJS) $(TARGET).cpp -o $(TARGET)
# 编译规则
%.o: $(SRC_DIR)/%.cpp
g++ -I$(INC_DIR) -c $< -o $@
# 清理规则
clean:
rm -f $(OBJS) $(TARGET)
注意事项
构建一个过程可能有多条命令,如果直接换行,命令是在不同的 shell 上执行的,也就是命令直接不会相互影响,例如:
test:
export foo=bar
echo $$foo
这里 foo
的值是取不到的(两个 $
是因为 $
在 makefile 中是转义字符,要使用 $
就需要再转义一下)。如果要让多条命令在一个 shell 中执行,可以使用 \
或 ;
,例如:
test:
export foo=bar \
echo $$foo
# 另一种写法
test:
export foo=bar; echo $$foo
Reference
[1] Make 命令教程 - 阮一峰的网络日志 (ruanyifeng.com)