Makefile 入门指南

前言

之前接触并且使用过 Makefile,但是一直没有彻底的理解 Makefile 。最近的一个项目要在不同的架构运行时,之前一直是使用 .sh 文件进行代码编译和部署的。但后来要维护的 .sh文件越来越多,想着如何把这些文件合并到一个文件当中,这时突然就想到了 Makefile,所以才打算再重新学习并且深入理解一下 Makefile

Makefile

Makefile 一般是用于进行大型项目的编译和构建的。最常见的就是 C,C++ 项目的编译,其他的语言一般都会有自己的构建工具。在 CC++ 中,可以通过文件的改动与否来决定哪一部分需要重新编译,哪一部分不需要,这样可以节省时间和资源。当然,除了编译以外,Makefile 也可以执行一系列不同的指令,来生成对应的内容。Makefile 的本质就是要进行 make,而 make 的本意就是制作。根据依赖的文件和对应的指令 make 出目标文件

举个例子,创建一个文件名为 Makefile 的文件,写入以下内容:

bash 复制代码
hello.txt:
	echo "Hello world!" > hello.txt

在终端执行 make 之后,上面的例子就会在当前目录下 make 出一个 hello.txt 的文件,文件的内容就是 Hello word!。由此可见,make 的本意就是制作,根据命令制作出目标文件。

Makfile 语法

Makefile 有一套自己的语法规则,常见的 Makfile 的规则如下:

bash 复制代码
targets: prerequisites
	command
	command
	command
  • targets:用空格分开的文件名,一般只有一个
  • prerequisites: 也是空格分开的文件名,它是 targets 依赖的文件。这些文件要在命令 make 目标文件 targets时存在
  • command:一系列的用于 make 目标文件 targets 的命令。要注意的是,在 command 的最前头是一个 tab,而不是空格

下面通过一个例子来解释一下不同部分具体的含义:

bash 复制代码
hello.txt:exmaple.txt
        cat exmaple.txt > hello.txt

保存之后,我们在终端执行 make hello.txt,就会生成一个 hello.txt 文件,文件的内容就是 exmaple.txt的内容。在这个例子中,

  • targethello.txt
  • make 的目标 hello.txt 的依赖文件为 exmaple.txt
  • make 时要执行的命令就是 cat exmaple.txt > hello.txt,读取 exmaple.txt 文件的内容,写入到 hello.txt 文件

上面的例子中 exmaple.txt 文件要在执行 make 之前存在于该目录下面,否则 make 就会执行失败

Makefile 的本质

上面的例子简单的介绍了 Makefile。下面通过一个复杂一点的例子来看看,Makefile 的运行原理。因为 Makefile 非常适合编译 C,C++ 语言,那么就用来编译一个 C 语言的例子

首先,我们创建一个 blah.c 的文件,如下:

csharp 复制代码
// blah.c
int main() { return 0; }

接着,我们编写 Makefile 来编译 C 程序,如下:

makefile 复制代码
blah:
	cc blah.c -o blah

我们运行 make(如果在 make 之后没有指定目标参数,那么文件中的第一个目标对应的命令就会运行)。运行完成之后,就会编译出一个 blah 程序。如果我们再运行一次时,就会出现 make: 'blah' is up to date 的提示了。即便我们重新更改了 blah.c 之后,再执行 make 还是会出现同样的提示,除非再 make 之前删除 blah 程序

为了有一种更好的体验,我们可以对 Makefile 进行如下的更改,

makefile 复制代码
blah: blah.c
	cc blah.c -o blah

这个例子和上面的区别就是给目标 blah 添加了一个依赖 blah.c。此时,我们再次 make 还是会产生一个 blah 文件。如果我们不更改 blah.c ,再次执行 make,还是会出现 make: 'blah' is up to date 的提示,这说明 blah 程序是最新的,无需重新编译。

如果我们更改 blah.c 的内容,再执行 make,那么就会执行成功。并且用新产生的 blah 文件覆盖掉之前的 blah 文件。这就是 make 的本质,当目标文件的依赖文件不变时,重新 make 时,对应的命令不会执行。而目标文件的依赖若是变了,重新编译就会产生新的目标文件且覆盖掉之前的目标文件

make 是如何觉察目标的依赖变了还是没变呢?它是根据文件的时间戳,每次 make 之后都会记下生成当前目标文件时所有依赖文件的时间戳。下次 make 时,如果依赖的时间戳变了就会重新 make,否则就不会。但是,此处有一个问题,就是如果你更改了文件,并且更改了文件的时间戳为之前的时间戳,那么 make 就无法正确的判断文件是否发生了变化

目标依赖其他目标

在上面的例子中,目标依赖的是其他的目标。同样的,目标也可以依赖其他的目标。如下面的例子所示:

makefile 复制代码
blah: blah.o
	cc blah.o -o blah # Runs third

blah.o: blah.c
	cc -c blah.c -o blah.o # Runs second

# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
	echo "int main() { return 0; }" > blah.c # Runs first

这个例子中,blah 依赖 blah.oblah.o 又依赖 blah.c。当我们在执行 make 时,首先会去执行第一个目标 blah 的命令,因为看到这个目标依赖 blah.o,所以就会转去执行 blah.o,而在执行 blah.o 时发现目标依赖 blah.c,那么就会再去执行 blah.c。等 blah.c 完成之后,再会依次执行 blah.oblah

在这个例子中,为了 make 获得 blah,产生了一堆中间文件 blah.c, blah.o。所以执行完成之后需要清理,也可以通过 Makefile 实现,具体如下:

makefile 复制代码
blah: blah.o
	cc blah.o -o blah # Runs third

blah.o: blah.c
	cc -c blah.c -o blah.o # Runs second

# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
	echo "int main() { return 0; }" > blah.c # Runs first

clean:
	rm blah.c blah.o

在执行完 make 之后,再执行 make clean 就会清楚产生的中间文件了。如果你闲麻烦,不想执行两次 make ,也可以使用 all

ALL

为了能够一次 make完成编译,并且在编译之后清除中间文件,可以更改 Makefile 如下:

makefile 复制代码
all: blah clean

blah: blah.o
	cc blah.o -o blah # Runs third

blah.o: blah.c
	cc -c blah.c -o blah.o # Runs second

# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
	echo "int main() { return 0; }" > blah.c # Runs first

clean:
	rm blah.c blah.o

在上面的例子中,我们添加一个目标 all,然后它的依赖就是其他下面两个目标。那么,此时我们执行 make all,就会按顺序执行下面的两个命令了,这样就不需要 make两次了

.PHONY

在上面介绍了 Makefile 的语法和一些简单的例子,下面通过更多的例子来揭露 Makefile 的本质。如下面这个例子:

bash 复制代码
hello:
	echo "Hello, World"

我们运行 make hello,只要 hello 文件不存在,这个命令就会运行。这个 Makefile 中的 target hello 和之前的那个 target hello.txt 不太一样。这个 Makefilemake hello 之后,只是在终端输出 Hello, World,不会去创建 hello 文件。但是,此处有一个问题,如果在 Makefile 同级目录下面正好有一个 hello 的文件,那会怎么样呢?

如果在同级目录下面 make hello 之前,已经存在一个 hello 文件了,那么这个命令就不会执行了。因为要 make 的目标已经存在了,而且是最新的,按照 Makefile 的规则就不会再会去 make hello 了。为了解决这个问题,就需要引入一个新的关键字 .phony 了。

我们还以上面的例子来说明,加入现在同级目录下面已经存在一个 hello 文件了,为了使得上面的命令能够继续执行,我们需要修改 Makefile 如下:

makefile 复制代码
.PHONY: hello

hello:
	echo "Hello, World"

修改保存之后,此时再运行 make hello 就会执行命令,在终端输出 Hello World 了。上面的例子中的 .PHONY 的作用就是告诉 make 这是一个虚拟目标,不会产生该目标的文件,所以就不会受到统计目录下面 hello 文件名的影响了

Makfile 中的变量

Makefile 中也可以使用变量,一遍变量的声明方式为 :=,直接使用 = 也可以。下面是一个使用变量的例子:

makefile 复制代码
files := file1 file2
some_file: $(files)
	echo "Look at this variable: " $(files)
	touch some_file

file1:
	touch file1
file2:
	touch file2

clean:
	rm -f file1 file2 some_file

变量的引用一般是使用 ${} 或者 $()。如下面的例子:

bash 复制代码
x := dude

all:
	echo $(x)
	echo ${x}

	# Bad practice, but works
	echo $x 

Makefile 中有一些常见的内置变量,如下所示:

  • $@: 表示当前目标文件的名称。
  • $^: 表示所有的依赖文件,以空格分隔。
  • $<: 表示第一个依赖文件。
  • $*: 表示目标文件名称的主干部分(不包含扩展名),比如:目标 hello.c ,那么 $* 表示 hello
  • MAKE: make命令本身的名称。
  • MAKECMDGOALS: 命令行中指定的目标列表。
  • MAKEFILES: 包含被载入的Makefile的文件列表。
  • MAKELEVEL: 当前Make的嵌套深度。
  • MAKEFILE_LIST: 已读取的Makefile文件列表。
Makefile 模式匹配

Makefile 中支持两种模式匹配符号:*%* 表示在你的文件系统重搜索匹配的文件名称,并且经常和 wildcard 函数一起使用。* 可以用在目标,依赖或者是 wildcar 函数中。一般不建议单独使用,you如下一些原因:

  • 单独使用时,* 是在shell 中展开,而不是在 make 中展开
  • 不同的操作系统或者shell 会有不同的表现,wildcard 提供了一致的行为
  • 单独使用时,如果匹配不到值时,* 会被原样保留。而使用 wildcard 函数会返回空列表
  • wildcard 提供了更加复杂的模式匹配,比如:$(wildcard *.c *.h)

所以,一般使用时:

ini 复制代码
# 不推荐:可能无法正确展开或在某些情况下出错
SRCS = *.c

# 推荐:使用 wildcard 函数
SRCS = $(wildcard *.c)
% 通配符

% 通配符中,有两种类型的操作:模式匹配和字符串替换。在模式匹配当中,被匹配到的部分 stem(词干)。可以在目标和依赖中应用模式匹配 %。也可以用 $*引用 stem。下面具体例子说明:

makefile 复制代码
%.o: %.c
    gcc -c $< -o $@

在这个例子中:

  • %.o 是目标模式
  • %.c 是依赖模式
  • % 匹配相同的字符串(stem)在目标和依赖中

假设我们有一个文件叫 main.c,当 Make 处理它时:

  • % 匹配 "main"
  • "main" 就是这个规则的 stem
  • $< 展开为 main.c(第一个依赖)
  • $@ 展开为 main.o(目标)

所以这个规则实际上会执行:

css 复制代码
gcc -c main.c -o main.o

stem 的概念允许我们编写通用的规则,可以应用于多个文件,而不需要为每个文件单独写规则。

一些进阶用法:

  1. 在命令中使用 stem:

    makefile 复制代码
    %.o: %.c
        @echo "Compiling $* (stem)"
        gcc -c $< -o $@
  2. 在依赖中使用 stem:

    makefile 复制代码
    %.o: %.c %.h
        gcc -c $< -o $@
  3. 复杂模式:

    makefile 复制代码
    lib%.a: %.o
        ar rcs $@ $

    这会将 example.o 转换为 libexample.a。

下面是一些字符替换的例子:

go 复制代码
sources := foo.c bar.c baz.c
objects := $(sources:.c=.o)

这里 $(sources:.c=.o) 表示将 sources 变量中所有 .c 结尾的文件名替换为 .o 结尾。结果是 objects 变成 "foo.o bar.o baz.o"

下面是一个更加复杂的例子:

go 复制代码
files := a.txt b.txt c.txt
newfiles := $(files:%.txt=%.md)

这将 files 中的所有 .txt 文件替换为 .md 文件。newfiles 的值将是 "a.md b.md c.md"。

将模式匹配和字符替换结合

makefile 复制代码
prog_sources := $(wildcard *.c)
prog_objects := $(prog_sources:.c=.o)

%.o: %.c
    gcc -c $< -o $@

program: $(prog_objects)
    gcc $^ -o $@

这个例子结合了通配符(wildcard)、模式替换和模式规则:

  • 首先找到所有 .c 文件
  • 然后生成对应的 .o 文件列表
  • 使用模式规则编译每个 .c 文件
  • 最后链接所有 .o 文件生成最终的程序

stem 的概念使得 Makefile 能够更灵活地处理文件名模式,大大简化了构建规则的编写。

Makefile 的运行环境

Makefile 中,每一行命令默认都是在一个新的 shell 实例中执行的,这意味着:

  • 每条命令都在一个全新的环境中运行
  • 命令之间没有状态共享
  • 一行中的多个命令(用分号分隔)会在同一个 Shell 中执行

下面具体例子来说明:

bash 复制代码
one: 
	cd ..
	# The cd above does not affect this line, because each command is effectively run in a new shell
	echo `pwd`

two:
	# This cd command affects the next because they are on the same line
	cd ..;echo `pwd`

three:
	# Same as above
	cd ..; \
	echo `pwd`

目标 one是进入上层目录,然后打印上层的绝对路径,但因为 makefile 中每一行命令都是在一个新的 shell 中执行的,所以 one目标的 echo pwd 不会打印上层目录的绝对路径,而是当前目录的绝对路径。要实现以上功能,可以使用 make two。它的命令是在同一行,中间用 ; 分隔开。目标 three 和目标 two 效果相同

Makefile 中默认的shell 是 /bin/sh,也可以通过指定变量 SHELL 来更改:

bash 复制代码
SHELL=/bin/bash

cool:
	echo "Hello from bash"

参考:

相关推荐
朝九晚五ฺ43 分钟前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream1 小时前
Linux的桌面
linux
xiaozhiwise1 小时前
Makefile 之 自动化变量
linux
意疏4 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu4 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿4 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K84095 小时前
Flink整合Hudi及使用
linux·服务器·flink
1900435 小时前
linux6:常见命令介绍
linux·运维·服务器
Camellia-Echo5 小时前
【Linux从青铜到王者】Linux进程间通信(一)——待完善
linux·运维·服务器