简介
Makefile是什么?
Makefile是一种定义了一系列规则来指定源代码文件之间依赖关系,以及如何生成目标文件的文件。它是一种自动化构建工具,可以帮助程序员自动化编译、链接和打包程序。
Makefile的作用
Makefile主要用于管理和组织软件项目的编译过程。通过Makefile,我们可以将整个项目分解为多个模块,并定义每个模块之间的依赖关系。这样,当某个模块发生变化时,只需要重新编译该模块及其依赖的其他模块即可,从而提高了软件开发的效率。
基本语法
规则和命令
Makefile 中的规则由目标、依赖和命令三部分组成。例如下面是一个简单的规则:
target: dependency1 dependency2
command1
command2
其中 target
表示目标文件,dependency1
和 dependency2
表示依赖文件,command1
和 command2
表示构建命令。
变量
Makefile中的变量可以用于存储常量、路径、编译器选项等信息。其基本语法如下:
variable_name = variable_value
其中,variable_name
表示变量名,variable_value
表示变量的值。
例如下面是一个使用变量的规则:
CC = gcc
CFLAGS = -Wall -O2
target: dependency.c
$(CC) $(CFLAGS) -o target dependency.c
其中 CC
和 CFLAGS
是变量,$(CC)
和 $(CFLAGS)
是变量引用。-Wall
和 -O2
是编译选项,可以通过 CFLAGS
定义。$(CC)
和 $(CFLAGS)
会被自动展开为 gcc
和 -Wall -O2
。
注释和空行
- 注释可以用 # 开头,用于解释 makefile 的内容。
#这是一条注释...
- 空行可以用来分隔不同的规则或者命令,提高可读性。
all: ... clean: ...
常用命令
make
make命令用于执行Makefile中的规则,生成目标文件。
clean
clean命令用于删除所有生成的目标文件和中间文件。
all
all命令用于编译整个项目,并生成可执行文件。
install
install命令用于安装可执行文件到指定的位置。
高级特性
自动变量
自动变量是一种特殊的变量,它们表示了Makefile中的一些常见操作,如编译器、源文件、目标文件等。常用的自动变量包括$@
、$<
、$^
等。
$@
:代表规则中的目标文件名。$<
:代表规则中的第一个依赖文件名。$^
:代表规则中所有的依赖文件名,以空格分隔。$?
:代表规则中所有比目标文件更新的依赖文件名,以空格分隔。$*
:代表规则中目标文件名去掉后缀之后的部分。
内置函数
Makefile内置函数是一些由make工具提供的函数,用于对变量进行操作、字符串处理、文件名处理等。常用的Makefile内置函数包括:
- $(subst from,to,text):将text中的from替换为to。
- $(patsubst pattern,replacement,text):对text中符合pattern的部分进行替换,替换成replacement。
- $(wildcard pattern):查找当前目录下符合pattern的文件列表。
- $(shell command):执行command命令,并返回其输出结果。
- $(strip string):去除string两端的空格。
- $(foreach var,list,text):对list中的每个元素var,都执行一遍text。
- $(if condition,then-part[,else-part]):如果condition满足,则执行then-part,否则执行else-part。
- $(call function,param1,param2,…):调用自定义函数function,并传递参数param1、param2等。
这些内置函数可以大大简化Makefile的编写,提高Makefile的可读性和可维护性。同时,也可以结合使用多个内置函数,实现更加复杂的功能。
以下是一个使用内置函数的简单例子:
SOURCES := $(wildcard src/*.c)
OBJECTS := $(patsubst src/%.c, obj/%.o, $(SOURCES))
all: myprogram
myprogram: $(OBJECTS)
gcc $^ -o $@
obj/%.o: src/%.c
gcc -c $< -o $@
clean:
rm -f $(OBJECTS) myprogram
在这个例子中,我们使用了两个内置函数:$(wildcard)
和$(patsubst)
。
第一行代码使用$(wildcard)
函数查找所有src/
目录下的.c
文件,并将它们的路径保存到变量SOURCES
中。
第二行代码使用$(patsubst)
函数将SOURCES
中的每个.c
文件路径替换为对应的.o
文件路径,并将它们保存到变量OBJECTS
中。
这样,我们就得到了所有需要编译的.o
文件列表。接下来,在myprogram
目标中,我们使用$^
自动展开$(OBJECTS)
,并将其作为依赖项编译出可执行文件myprogram
。
模式规则
模式规则是一种通配符规则,可以匹配多个文件。其基本语法如下:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
其中,%.o
表示所有以.o
结尾的目标文件,%.c
表示所有以.c
结尾的依赖文件。
静态模式
Makefile的静态模式是一种特殊的规则格式,它可以用来指定多个目标文件和依赖文件之间的关系,从而简化Makefile的编写。静态模式的语法如下:
<targets...>: <target-pattern>: <prereq-patterns...>
<recipe>
其中,<targets...>
表示一组目标文件名,可以包含通配符;<target-pattern>
表示目标文件名的模式,也可以包含通配符;<prereq-patterns...>
表示一组依赖文件名的模式,可以包含通配符;<recipe>
表示生成目标文件的命令。
静态模式的作用在于,它可以将多个类似的规则合并为一个,从而使Makefile更加简洁和易于维护。例如,下面是一个使用静态模式的示例:
objects = foo.o bar.o baz.o
all: $(objects)
$(objects): %.o: %.c
gcc -c $< -o $@
在这个示例中,$(objects)
表示一组目标文件名,即foo.o
、bar.o
和baz.o
。使用静态模式,可以将它们的编译规则合并为一个,避免了重复的代码。具体来说,$(objects): %.o: %.c
表示所有以.o
结尾的目标文件都依赖于同名的.c
文件,生成目标文件的命令是gcc -c $< -o $@
,其中$<
表示依赖文件名,$@
表示目标文件名。
实例演示
编写一个简单的 Makefile 文件
下面是一个简单的 Makefile 文件,它定义了一个目标文件 hello
,依赖于源文件 hello.c
,并且通过 gcc
命令进行编译和链接。
CC = gcc
hello: hello.c
$(CC) -o hello hello.c
可以通过 make hello
命令执行该 Makefile 文件,生成可执行文件 hello
。
构建一个 C/C++ 项目
对于一个 C/C++ 项目,通常需要编译多个源文件,并且需要链接静态库或动态库。下面是一个简单的 C 项目的 Makefile 文件:
CC = gcc
CFLAGS = -Wall -O2
LIBS = -lm
SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRCS))
TARGET = $(BIN_DIR)/myapp
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LIBS)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c -o $@ $<
该 Makefile 文件定义了三个目录变量 SRC_DIR
、OBJ_DIR
和 BIN_DIR
,分别表示源代码目录、目标文件目录和可执行文件目录。SRCS
和 OBJS
分别表示源文件列表和目标文件列表,可以通过内置函数 wildcard
和 patsubst
自动生成。
该 Makefile 文件还定义了两个规则,分别用于编译目标文件和链接可执行文件。其中 $@
表示目标文件名,$^
表示所有依赖文件列表,$<
表示第一个依赖文件名。
进阶技巧
高级变量和函数
Makefile 支持高级变量和函数,例如条件变量、函数调用和字符串操作等。下面是一个使用条件变量和函数调用的 Makefile 文件:
ifeq ($(OS),Windows_NT)
RM = del /q
else
RM = rm -f
endif
.PHONY: clean
clean:
$(RM) *.o
该 Makefile 文件定义了一个条件变量 OS
,根据当前操作系统类型设置不同的 RM
变量。同时,还定义了一个 .PHONY
规则,表示 clean
是一个伪目标,不会生成实际的文件。
条件语句和循环语句
Makefile 支持条件语句和循环语句,例如 if
、else
和 for
等。下面是一个使用 if
和 for
的 Makefile 文件:
CC = gcc
CFLAGS = -Wall -O2
TARGETS = hello world goodbye
all: $(TARGETS)
$(TARGETS):
$(CC) $(CFLAGS) -o $@ $@.c
clean:
rm -f $(TARGETS)
.PHONY: all clean
ifeq ($(DEBUG),1)
CFLAGS += -g
endif
for:
@for i in 1 2 3; do \
echo $$i; \
done
该 Makefile 文件定义了一个 all
规则和一个 clean
规则,分别用于编译所有目标文件和清除所有目标文件。同时,还定义了一个 ifeq
条件语句,根据 DEBUG
变量的值设置调试选项。
该 Makefile 文件还定义了一个 for
规则,使用 for
循环输出数字 1、2 和 3。
模块化设计和代码重用
Makefile 支持模块化设计和代码重用,可以将一些通用的规则和变量定义在单独的文件中,并通过 include
命令引入。例如下面是一个使用 include
的 Makefile 文件:
CC = gcc
CFLAGS = -Wall -O2
TARGETS = hello world goodbye
all: $(TARGETS)
$(TARGETS):
$(CC) $(CFLAGS) -o $@ $@.c
clean:
rm -f $(TARGETS)
.PHONY: all clean
include common.mk
该 Makefile 文件通过 include
命令引入了名为 common.mk
的文件,该文件定义了一些通用的变量和规则,例如 CC
、CFLAGS
和 clean
等。
生成依赖关系
在Makefile中,我们可以使用“依赖关系”来描述源文件之间的关系。
手动构建依赖关系
例如,如果我们有一个名为“prog.c”的源文件,它依赖于“util.h”和“defs.h”两个头文件,那么可以这样写:
prog.o: prog.c util.h defs.h
gcc -c prog.c -o prog.o
这里,“prog.o”表示编译生成的目标文件,“prog.c”、“util.h”和“defs.h”表示依赖的源文件。当我们运行make命令时,Makefile会自动检查这些依赖关系,如果其中的任何一个文件发生了变化,就会重新编译“prog.o”。
自动构建依赖关系
但是通过上面的方法构建依赖关系,在大型项目里肯定是不现实的,所以我们可以换一种方法来构建这些文件的依赖关系,以下是一个自动生成依赖关系示例:
CC = gcc
CFLAGS = -Wall -g
-include $(OBJECTS:.o=.d)
$(BINDIR)/program: $(OBJECTS)
$(CC) $(CFLAGS) $^ -o $@
$(OBJDIR)/%.o: $(SRCDIR)/%.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
其中,-include
表示包含依赖关系文件,.d
表示依赖关系文件后缀名,-MMD
表示生成依赖关系文件,-MP
表示生成空的依赖关系文件。
常见问题和解决方案
无法生成目标文件
如果Makefile无法生成目标文件,可以检查以下几点:
- 目标文件和依赖文件是否正确。
- 编译器选项是否正确。
- Makefile中是否存在语法错误。
头文件依赖错误
如果头文件依赖出现错误,可以尝试使用gcc -MM
命令生成依赖关系文件,并将其包含到Makefile中。
如何调试 Makefile?
可以使用make -n
命令查看Makefile执行过程中的命令行,或者使用make -d
命令查看Makefile的详细调试信息。
Makefile和CMake的比较
Makefile和CMake都是常用的构建工具,它们有一些相同之处,但也有许多不同之处。
Makefile
Makefile是一种基于规则的构建工具,最初是为Unix操作系统设计的。它使用一个名为Makefile的文件来描述如何编译和链接代码,并且可以自动化整个构建过程。Makefile中定义了一系列规则,每个规则指定了一个或多个目标文件以及生成该目标文件所需的依赖关系和命令。
优势
- Makefile可以直接调用shell命令,非常灵活。
- Makefile支持很多编程语言,包括C/C++、Java、Python等。
- Makefile可以根据时间戳判断哪些文件需要重新编译,从而提高构建效率。
缺点
- Makefile的语法比较繁琐,需要手动编写很多规则。
- Makefile不够跨平台,需要为不同的操作系统编写不同的规则。
- Makefile不支持自动生成依赖关系,需要手动维护依赖关系。
应用场景
- 简单的项目,特别是只有一两个源文件的项目。
- 需要对构建过程进行精细控制的项目。
- 对构建速度要求不高的项目。
CMake
CMake是一种跨平台的构建工具,可以生成Makefile、Visual Studio项目等多种构建文件。CMake使用一个名为CMakeLists.txt的文件来描述如何构建代码,并且可以自动检测系统环境和依赖关系。
优势
- CMake语法比较简单,易于学习和使用。
- CMake可以根据不同的操作系统自动生成相应的构建文件。
- CMake支持自动生成依赖关系,减少了手动维护依赖关系的工作量。
缺点
- CMake不够灵活,不能直接调用shell命令。
- CMake的速度比Makefile慢一些。
- CMake的文档比较复杂,需要花费一定的时间来学习。
应用场景
- 复杂的项目,特别是包含多个源文件和库的项目。
- 跨平台的项目,特别是需要在Windows和Linux等不同操作系统上构建的项目。
- 对构建速度要求高的项目。
总的来说,如果你需要对构建过程进行精细控制或者对构建速度要求不高,那么可以选择Makefile;如果你需要跨平台构建或者对构建速度有较高要求,那么可以选择CMake。当然,两种构建工具也可以结合使用,比如使用CMake生成Makefile,以兼顾两者的优点。