makefile是什么,它是如何工作的?
原文:https://opensource.com/article/18/8/what-how-makefile
概述
当某些文件发生改变时想要执行一个执行一个任务时,make
可以排上用场。Make
需要一个文件名为makefile
或MakeFile
的文件来定义一系列将要运行的任务集。你可以使用make来编译程序源码。大多数开源项目使用make
来编译最终二进制可执行程序,在程序安装时可以使用make install
来进行安装。
在这篇文章中,我们根据基础和高级的例子来对make
和makefile
来进行探索。在开始之前,确保系统已经安装了make
。
基础示例
让我从终端中打印经典的Hello world开始。创建一个空的文件夹 my-project
,在文件夹中创建makefile
文件并写入以下内容:
say_hello:
echo "Hello World"
现在我们可以在myProject
文件夹中输入make
来运行makefile
文件。执行完毕之后,终端会输出以下内容:
lyra@lyra:~/my-project$ make
echo "hello world"
hello world
在上述例子中,say_hello
与其他编程语言一样,定义了一个方法名称,这个名称被成为target。在target中可以定义一些先决条件和依赖项。为了简单起见,我们并没有在例程中定义先决条件和依赖项。echo "Hello World"
被称为recipe,类似编程语言中的函数体。target、先决条件/依赖项、recipe共同构成一个makefile
规则,以下是一个典型的makefile
语法规则:
target: prerequisites
<TAB> recipe
如下例子所示,taget是一个二进制文件并依赖于先决条件(源代码文件)。另一方面,先决条件也可以是依赖于其他依赖项的target
.
final_target的先决条件是sub_target和final_target.c,而sub_target依赖于其他依赖项sub_target.c
final_target: sub_target final_target.c
Recipe_to_create_final_target
sub_target: sub_target.c
Recipe_to_create_sub_target
没有依赖文件且不生成与目标文件同名的文件被称为伪目标
返回我们之前的例子,当我们执行make命令时,会将 echo "Hello World"
全部输出,后面紧跟着命令输出。我们通常不希望将命令回显到终端中来,可以在echo前面添加@
符号:
say_hello:
@echo "Hello World"
让我们在执行make
命令试一下,应该会在终端输出这些内容:
lyra@lyra:~/my-project$ make
Hello Lyra
让我们再添加两个伪目标:generate和clean
say_hello:
@echo "Hello Lyra"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
如果我们再修改完文件之后再次尝试运行make
时,只有targetsay_hello
会被执行。因为makefile会将第一个target识别为默认target,在all这个target中需要调用其他的target来进行项目编译操作,这就是为什么大多数项目第一个target为all
的原因。我们可以使用.DEFAULT_GOAL
来指定我们项目中的默认target
在makefile文件开头编写以下内容
.DEFAULT_GOAL := generate
DEFAULT_GOAL一次只能执行一个target
然我们把DEFAULT_GOAL
替换城all
,在调用make
命令之前,我们先添加一个.PHONY
伪目标,这个伪目标可以设置一些非生成文件的target。我们设置了一个target,用于清除后缀为.txt的文件,一般情况下,直接执行make clean
则会输出以下内容:
lyra@lyra:~/my-project$ make clean
Cleaning up...
rm *.txt
当文件夹下有个与clean target同名的文件时,调用则会无法删除文件并输出以下内容
lyra@lyra:~/my-project$ make clean
make: 'clean' is up to date.
lyra@lyra:~/my-project$
这是因为make
默认将taget识别为会生产文件的target,而文件夹下已经有了名称为clean的文件,除非文件发生修改,否则命令无法执行,clean target的本意是清理文件,这样与我们的业务功能不相符,所以要使用.PHONY
来定义不生成文件的target,添加之后再执行的makefile内容如下:
.PHONY: clean
say_lyra:
@echo "Hello Lyra"
generate: clean
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
让我们再调用make clean
命令则会一切正常并输出以下内容:
lyra@lyra:~/my-project$ make clean
Cleaning up...
rm *.txt
rm: cannot remove '*.txt': No such file or directory
高级
变量
在上面的一些例子中,大多数target和前提条件值都使用硬编码,但是在真实项目中,这些硬编码需要替换城变量或表达式。
在makefile中定义变量的最简单的方法是使用=
操作符来定义变量。例如,使用CC
变量定义了一个gcc命令:
CC = gcc
只用一个=
定义的变量被称为递归扩展变量,可以如下方式使用递归变量:
compile : hello.c
${CC} hello.c -o hello
使用时${CC} hello.c -o hello
与 gcc hello.c -o hello
同义
在使用递归扩展变量时如果使用了其他变量的内容则会替换这些内容,如下例子所示,echo ${foo}是首先递归调用bar变量中的值,而bar变量中的值又从上层ugh变量中获取,foo = bar = ugh。
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
输出:
lyra@lyra:~/my-project$ make
echo Huh?
Huh?
${CC}和$(CC)都是正确的gcc引用。但是如果将变量重新赋值给它本身的话,将会引发死循环,让我们来验证一下,
CC = gcc
CC = ${CC}
all:
@echo ${CC}
执行make
将会输出以下结果:
lyra@lyra:~/my-project$ make
makefile:2: *** Recursive variable 'CC' references itself (eventually). Stop.
为了避免这种情况,可以使用:=
操作符来定义简单扩展变量,简单扩展变量只会展开一次,所以不会出现递归死循环的问题,我们尝试运行以下makefile不出意外的话,应该没什么问题
CC = gcc
CC := ${CC}
complie : hello.c
${CC} hello.c -o hello
表达式和方法
让我们根据makefile编译c程序并逐行探讨规则含义。
# Usage:
# make # compile all binary
# make clean # remove ALL binaries and objects
.PHONY = all clean
CC = gcc # compiler to use
LINKERFLAG = -lm
SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)
all: ${BINS}
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@
%.o: %.c
@echo "Creating object.."
${CC} -c $<
clean:
@echo "Cleaning up..."
rm -rvf *.o ${BINS}
- # 定义了一行注释
- .PHONY = all clean定义了两个伪目标分别是all和clean,避免生产clean和all文件导致命令执行失败,所以使用了这条语句。
- LINKERFLAG定义了一个变量,使用时替换gcc参数。
- SRCS := $(wildcard *.c): $(WILDCARD pattern) 是一个文件名称函数,在这条语句中,所有以.c文件为后缀的文件都会传入到SRCS变量中。利用echo打印会输出以下内容:
lyra@lyra:~/my-project$ make
echo hello.c md.c papa.c
hello.c md.c papa.c
- BINS := $(SRCS:%.c=%)被称为替换引用,在本节中,如果SRCS包含foo.c bar.c, 那么BINS被替换成foo bar
将%.c 替换城% 比如hello.c替换城hello
输出:
lyra@lyra:~/my-project$ make
echo hello md papa
hello md papa
- all: ${BINS} 将前提条件替换为一个变量BINS
- 规则:
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $<; -o $@
让我们根据一个例子来理解这个规则。%可以匹配任意target名称,%.o可以匹配所有后缀为.o的文件,
$<标识第一个前提条件的名称,也就是%.o,$@表示target的名称,也就是%。如果foo
是${BINS}
传入的值,那么,%
将会被替换城foo
,下面是规则的展开形式:
foo: foo.o
@echo "Checking.."
gcc -lm foo.o -o foo
- 规则:
%.o: %.c
@echo "Creating object.."
${CC} -c $<;
每个target的前提条件是本规则,下面是规则的展开形式:
foo.o: foo.c
@echo "Creating object.."
gcc -c foo.c
- clean target可以根据后缀将文件进行删除
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码