使用 GCC 命令行进行程序编译,在单个文件下是比较方便的。但当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。这种情况下,需要借助项目构造工具 make 来帮助我们完成这个艰巨的任务。

make 是一个命令工具,一个解释 Makefile 中指令的命令工具make 工具在构造项目时需要加载一个 Makefile 文件,Makefile 关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,Makefile 定义了一系列的规则来指定哪些文件需要先编译、哪些文件需要后编译、哪些文件需要重新编译,甚至于进行更复杂的功能操作。Makefile 就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。

Makefile 带来的好处就是“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。

Makefile 文件有两种命名方式 Makefilemakefile。构建项目时,在哪个目录下执行构建命令 make,则这个目录下的 Makefile 文件就会被加载。因此,在一个项目中可以有多个 Makefile 文件,分别位于不同的项目目录中。

本文转载并修改自:https://subingwen.cn/linux/makefile/

规则

Makefile 的框架是由规则构成的。make 命令执行时先在 Makefile 文件中查找各种规则,对各种规则进行解析后运行规则。规则的基本格式为:

1
2
3
4
5
# 每条规则的语法格式(command 前为 Tab 缩进,不能是空格):
target1 target2 ...: depend1 depend2 ...
command1
command2
......

每条规则由三个部分组成分别是 目标 (target) 依赖 (depend)命令(command)

  • 命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令。
    • 例如:通过某个命令编译文件、生成库文件、进入目录等。
    • 动作可以是多个,每个命令前必须有一个 Tab 缩进并且独占占一行。
  • 依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。
    • 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用。
    • 如果规则的命令中不需要任何依赖,那么规则的依赖可以为空。
    • 当前规则中的依赖,可以是其他规则中的某个目标,这样就形成了规则之间的嵌套。
    • 依赖可以根据要执行的命令的实际需求,指定很多个。
  • 目标(target):规则中的目标,这个目标和规则中的命令是对应的。
    • 通过执行规则中的命令,可以生成一个和目标同名的文件。
    • 规则中可以有多个命令,因此可以通过这多条命令来生成多个目标,所以目标也可以有很多个。
    • 通过执行规则中的命令,可以只执行一个动作、但不生成任何目标,这样的目标被称为 伪目标

关于上面的解释可能有些晦涩,下面通过一个例子来阐述一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 举例: 有源文件 a.c b.c c.c head.h,需要生成可执行程序 app

################# 例 1 #################
app: a.c b.c c.c
gcc a.c b.c c.c -o app

################# 例 2 #################
### 有多个目标,多个依赖,多个命令 ###
app app1: a.c b.c c.c d.c
gcc a.c b.c -o app
gcc c.c d.c -o app1

################# 例 3 #################
### 规则之间的嵌套 ###
# 第一条规则
app: a.o b.o c.o
gcc a.o b.o c.o -o app

# a.o 是第一条规则中的依赖
a.o: a.c
gcc -c a.c

# b.o 是第一条规则中的依赖
b.o: b.c
gcc -c b.c

# c.o 是第一条规则中的依赖
c.o: c.c
gcc -c c.c

工作原理

在此主要为大家剖析一下通过提供的 Makefile 文件,构建工具 make 在什么时候编译项目中的所有文件,在什么时候只选择更新项目中的某几个文件。另外,再研究一下如果 Makefile 里有多个规则,它们之间是如何配合工作的。我们基于下边的例子,依次进行讲解。

规则执行

当调用 make 命令编译程序时,首先找到 Makefile 文件中的第 1 个规则,然后执行相关的动作。但需要注意的是,很多时候动作(命令)中使用的依赖可能不存在,如果依赖不存在,该动作也不会执行

对应的解决方案如下:

先将需要的依赖生成出来:在 Makefile 中添加新规则,将“不存在的依赖”作为目标,当新规则的命令执行完毕时,对应的目标就会生成。此时,其他规则中需要的依赖也就存在了。这样,某条规则在需要时会被其他规则调用,直到 Makefile 中的第一条规则的所有依赖都被生成。第一条规则中的命令可以基于这些依赖生成目标,完成 make 的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 规则之间的嵌套
# 规则 1
app: a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则 2
a.o: a.c
gcc -c a.c
# 规则 3
b.o: b.c
gcc -c b.c
# 规则 4
c.o: c.c
gcc -c c.c

在这个例子中,执行 make 命令会根据 Makefile 中的 4 条规则编译三个源文件。当解析第一条规则时,发现其中的三个依赖都不存在,因此对应的命令不能执行。

当依赖不存在时,make 会查找其他规则,找到用来生成这些依赖的规则,并执行其命令。因此,规则 2、规则 3、规则 4 中的命令会依次执行。当规则 1 中的依赖全部生成后,其对应的命令也会执行,最终生成规则 1 的目标,make 的工作就结束了。

知识点拓展:

如果想要执行 Makefile 中非第一条规则对应的命令,那么就不能直接 make,需要将那条规则的目标也写到 make 的后边,比如只需要执行规则 3 中的命令,就需要执行 make b.o

文件时间戳

在执行 make 命令时,会 根据文件的时间戳来判断 是否执行 Makefile 文件中相关规则中的命令。

  • 目标是通过依赖生成的,因此正常情况下,目标的时间戳应大于所有依赖的时间戳。如果执行 make 命令时检测到规则中的目标和依赖满足这个条件,则规则中的命令不会执行。
  • 当依赖文件被更新时,其时间戳也会随之更新。这时,目标的时间戳会小于某些依赖的时间戳,目标文件会通过规则中的命令被重新生成。
  • 如果规则中的目标对应的文件根本不存在,则规则中的命令必定会被执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 规则之间的嵌套
# 规则 1
app: a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则 2
a.o: a.c
gcc -c a.c
# 规则 3
b.o: b.c
gcc -c b.c
# 规则 4
c.o: c.c
gcc -c c.c

根据前文描述,首先执行 make 命令,根据 Makefile 编译这几个源文件生成对应的目标文件。然后修改例子中的 a.c 文件。再次执行 make 编译这几个源文件。在这种情况下,首先执行规则 2 更新目标文件 a.o,然后执行规则 1 更新目标文件 app。其余的规则不会被执行。

自动推导

make 是一个功能强大的构建工具,尽管我们在编写 Makefile 时可能会出现不够严谨的情况,导致漏写一些构建规则,但程序仍然可以成功编译。这是因为 make 具有自动推导的能力,不完全依赖于 Makefile。

举例来说,当使用 make 命令编译扩展名为 .c 的 C 语言文件时,源文件的编译规则无需明确给出。这是因为 make 在进行编译时会使用一个默认的编译规则,按照默认规则完成对 .c 文件的编译,生成对应的 .o 文件。默认情况下,它使用命令 cc -c 来编译 .c 源文件。在 Makefile 中,只需给出需要构建的目标文件名(即一个 .o 文件),make 会自动为这个 .o 文件寻找合适的依赖文件(对应的 .c 文件),并使用默认的命令来构建这个目标文件。

假设本地项目目录中有以下几个源文件:

1
2
3
4
5
6
7
8
9
$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── Makefile
├── mult.c
└── sub.c

目录中 Makefile 文件内容如下:

1
2
3
# 这是一个完整的 Makefile 文件
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

通过 make 构建项目:

1
2
3
4
5
6
7
$ make
cc -c -o add.o add.c
cc -c -o div.o div.c
cc -c -o main.o main.c
cc -c -o mult.o mult.c
cc -c -o sub.o sub.c
gcc add.o div.o main.o mult.o sub.o -o calc

可以观察到上述的 Makefile 文件中只有一条规则。在依赖部分,所有的 .o 文件在本地项目目录中都不存在,并且没有其他规则用来生成这些依赖文件。在这种情况下,make 会使用内部默认的构建规则,首先生成这些依赖文件,然后执行规则中的命令,最终生成目标文件 calc

变量

在使用 Makefile 进行规则定义时,为了增加灵活性,可以使用三种类型的变量:自定义变量、预定义变量、自动变量。

自定义变量

自定义变量:这些变量是用户自己定义的、没有类型,可以根据需要随时修改。通过定义变量,可以将一些常用的值或命令集中管理,以便在整个 Makefile 中重复使用。

1
2
3
4
# 错误,只创建了变量名,没有赋值
obj
# 正确,创建一个变量名并且给其赋值
LIBS = -lpthread

在给 Makefile 中的变量赋值之后,如何在需要的时候将变量值取出来呢?

1
$(LIBS)

自定义变量使用举例:

1
2
3
4
5
6
7
8
9
# 这是一个规则,普通写法
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 这是一个规则,里边使用了自定义变量
obj = add.o div.o main.o mult.o sub.o
target = calc
$(target): $(obj)
gcc $(obj) -o $(target)

预定义变量

预定义变量:这些变量是 make 已经定义好的,用户可以直接在 Makefile 中使用,而不用进行定义。例如,CC 表示 C 编译器的名称,CFLAGS 表示编译 C 程序时需要的额外参数等。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:

变 量 名 含 义 默 认 值
AR 生成静态库库文件的程序名称 ar
AS 汇编编译器的名称 as
CC C 语言编译器的名称 cc
CPP C 语言预编译器的名称 $(CC) -E
CXX C++ 语言编译器的名称 g++
FC FORTRAN 语言编译器的名称 f77
RM 删除文件程序的名称 rm -f
ARFLAGS 生成静态库库文件程序的选项 无默认值
ASFLAGS 汇编语言编译器的编译选项 无默认值
CFLAGS C 语言编译器的编译选项 无默认值
CPPFLAGS C 语言预编译的编译选项 无默认值
CXXFLAGS C++ 语言编译器的编译选项 无默认值
FFLAGS FORTRAN 语言编译器的编译选项 无默认

一个使用了自定义变量和预定义变量的 Makefile:

1
2
3
4
5
6
7
8
9
10
# 这是一个规则,普通写法
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 这是一个规则,里边使用了自定义变量和预定义变量
obj = add.o div.o main.o mult.o sub.o
target = calc
CFLAGS = -O3 # 代码优化
$(target): $(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)

自动变量

自动变量:这些变量的值由 make 在特定的上下文中自动赋值,无需用户手动定义。例如,在规则中使用 $@ 表示目标文件的名称,在命令中使用 $< 表示第一个依赖文件的名称等。自动变量使得在规则中引用目标文件、依赖文件等更加方便。

自动变量只能在规则的命令中使用,下表中是一些常见的自动变量:

变 量 含 义
$* 表示目标文件的名称,不包含目标文件的扩展名
$+ 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件
$< 表示依赖项中第一个依赖文件的名称
$? 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@ 表示目标文件的名称,包含文件扩展名
$^ 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开

下面几个例子,演示一下自动变量如何使用。

1
2
3
4
5
6
7
8
# 这是一个规则的普通写法
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 这是一个规则,在命令中使用了自定义变量来替换相关内容
# 自动变量只能在规则的命令中使用
calc: add.o div.o main.o mult.o sub.o
gcc $^ -o $@

模式匹配

在介绍概念之前,先读一下下面的这个 Makefile 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# gcc -c 仅编译 & 汇编、不链接
add.o: add.c
gcc add.c -c

div.o: div.c
gcc div.c -c

main.o: main.c
gcc main.c -c

sub.o: sub.c
gcc sub.c -c

mult.o: mult.c
gcc mult.c -c

在阅读过程中,能够发现从第二个规则开始到第六个规则做的是相同的事情。但是由于文件名不同,不得不书写多个规则,这就让 Makefile 文件看起来非常的冗余。我们可以将这一系列相同的操作整理成一个模板,所有类似的操作都可以通过模板去匹配。这样,Makefile 会精简不少,只是可读性会有所下降。

这个规则模板可以写成下边的样子,这种操作就称之为 模式匹配

1
2
3
4
5
6
7
8
# 第一个规则
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 模式匹配: 通过一个公式,代表若干个满足条件的规则
# 依赖有一个,后缀为.c,生成的目标是一个 .o 的文件,% 是一个通配符,匹配的是文件名
%.o: %.c
gcc $< -c

对于上述使用模式匹配的 Makefile,第一个规则中的依赖(这里是所有的 .o 目标文件)的生成,都需要基于这个使用了模式匹配的规则来生成。在这里,模式规则被执行了 5 次,其中的 % 对应的文件名是不断变化的。因此,命令中依赖的名字,必须要使用自动变量。

函数

Makefile 中有许多函数,它们都具有返回值。函数的格式与 C/C++ 中的函数不同,写法是 $(函数名 参数 1, 参数 2, 参数 3, ...),这样设计的目的是为了方便获取函数的返回值。

我将介绍两个在 Makefile 中使用频率较高的函数:wildcardpatsubst

wildcard

wildcard 函数的作用是在指定目录下获取特定类型的文件名列表,返回以空格分隔的文件名字符串。函数原型如下:

1
$(wildcard PATTERN...)
  • 参数功能:

    • PATTERN 指定了要搜索的目录和文件类型,比如 *.c 表示当前目录下的所有 .c 文件。
    • 可以指定多个目录,每个路径之间使用空格分隔。
  • 返回值:

    • 返回符合条件的文件列表,文件名之间使用空格分隔。
    • 例如:$(wildcard *.c ./sub/*.c) 可能返回 a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c

以下是函数的使用示例:

1
2
3
4
5
# 示例:搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c)

# 返回值:获取一个大字符串,包含了满足条件的文件名,文件名之间用空格分隔
# /home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

在这个示例中,src 变量获取了满足条件的文件列表,这些文件分别来自 /home/robin/a//home/robin/b/ 和当前目录。

patsubst

patsubst 函数的作用是替换指定模式的 文件名后缀,函数原型如下:

1
$(patsubst pattern, replacement, text)
  • 参数功能:

    • pattern:需要匹配的模式字符串,指定要被替换的文件名后缀。路径和文件名不需要关系,可以使用通配符 % 表示表示。
    • replacement:替换后的新后缀模式字符串。仍然使用 % 表示原始路径和文件名。
    • text:待处理的文本,即原始数据。
  • 返回值:

    • 函数返回替换后的字符串。

举例:$(patsubst %.c, %.o, file1.c file2.c) 会将 file1.cfile2.c 替换为 file1.ofile2.o

Makefile 编写

下面基于一个简单的项目,为大家演示一下编写一个 Makefile 从不标准到标准的进化过程。

1
2
3
4
5
6
7
8
9
# 项目目录结构
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
# 需要编写 Makefile 对该项目进行自动化编译

版本 1

1
2
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
  • 该版本的优点:书写简单。
  • 该版本的缺点:只要依赖中的某一个源文件被修改,所有的源文件都需要被重新编译,重新生成规则中的所有 .o 目标文件,太耗时、效率低。
  • 改进方式:提高效率,修改哪一个源文件,哪个源文件被重新编译,不修改就不重新编译。

版本 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 默认所有的依赖都不存在,需要使用其他规则生成这些依赖
# 因为 add.o 被更新,需要使用最新的依赖,生成最新的目标
calc: add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 如果修改了 add.c,add.o 被重新生成
add.o: add.c
gcc add.c -c

div.o: div.c
gcc div.c -c

main.o: main.c
gcc main.c -c

sub.o: sub.c
gcc sub.c -c

mult.o: mult.c
gcc mult.c -c
  • 该版本的优点:相较于版本 1 效率提升了,只需要重新生成被修改的源文件的 .o 目标文件。
  • 该版本的缺点:规则比较冗余,需要精简。
  • 改进方式:在 Makefile 中 使用变量和模式匹配

版本 3

1
2
3
4
5
6
7
8
9
# 添加自定义变量 
obj = add.o div.o main.o mult.o sub.o
target = calc

$(target): $(obj)
gcc $(obj) -o $(target)

%.o: %.c
gcc $< -c
  • 该版本的优点:文件精简不少,变得简洁了。
  • 该版本的缺点:变量 obj 的值需要手动的写出来,如果需要编译的项目文件很多,都用手写出来不现实。
  • 改进方式:在 Makefile 中 使用函数

版本 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加自定义变量
# 使用函数搜索当前目录下的源文件 .c
src = $(wildcard *.c)

# 将 .c 源文件的后缀替换为 .o
obj = $(patsubst %.c, %.o, $(src))

target = calc

$(target): $(obj)
gcc $(obj) -o $(target)

%.o: %.c
gcc $< -c
  • 该版本的优点:解决了自动加载项目文件的问题,解放了双手。
  • 该版本的缺点:没有文件删除的功能,不能删除项目编译过程中生成的 *.o 目标文件和可执行程序。
  • 改进方式:在 Makefile 文件中添加新的规则用于删除生成的 *.o 目标文件和可执行程序。

版本 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 添加自定义变量
# 使用函数搜索当前目录下的源文件 .c
src = $(wildcard *.c)

# 将 .c 源文件的后缀替换为 .o
obj = $(patsubst %.c, %.o, $(src))

target = calc

$(target): $(obj)
gcc $(obj) -o $(target)

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

# 添加规则,删除生成的目标文件和可执行程序
# 这个规则比较特殊,clean 根本不会生成,这是一个伪目标
clean:
rm $(obj) $(target)
  • 该版本的优点:添加了新的 clean 规则用于文件的删除,直接 make clean 就可以执行规则中的删除命令了。
  • 该版本的缺点:缺少对 clean 为伪文件的声明(在下面有具体的问题演示和分析)。
  • 该改进方式:在 Makefile 文件中声明 clean 是一个伪目标,让 make 放弃对它的时间戳检测。

正常情况下,这个版本的 Makefile 是可以正常工作的,但是如果在这个项目目录中添加一个叫做 clean 的文件(和规则中的目标名称相同),再进行 make clean 发现这个规则就不能正常工作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在项目目录中添加一个叫 clean 的文件,然后在 make clean 这个规则中的命令就不工作了 
$ ls
add.c calc div.c head.h main.o mult.c sub.c
add.o div.o main.c Makefile mult.o sub.o clean ---> 新添加的

# 使用 Makefile 中的规则删除生成的目标文件和可执行程序
$ make clean
make: 'clean' is up to date.

# 查看目录,发现相关文件并没有被删除,make clean 失败了
$ ls
add.c calc div.c head.h main.o mult.c sub.c
add.o clean div.o main.c Makefile mult.o sub.o

这个问题的关键点在于 clean 是一个伪目标,不对应任何实体文件,在前边讲 关于文件时间戳更新 问题的时候说过:如果目标不存在,规则的命令肯定被执行;如果目标文件存在了,就需要比较规则中目标文件和依赖文件的时间戳,满足条件才执行规则的命令,否则不执行。

解决这个问题需要在 Makefile 中声明 clean 是一个伪目标,这样 make 就不会对文件的时间戳进行检测,规则中的命令也就每次都会被执行了。

在 Makefile 中声明一个伪目标需要使用 .PHONY 关键字,声明方式为: .PHONY: 伪文件名称

最终版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 添加自定义变量
# 使用函数搜索当前目录下的源文件 .c
src = $(wildcard *.c)

# 将 .c 源文件的后缀替换为 .o
obj = $(patsubst %.c, %.o, $(src))

target = calc

$(target): $(obj)
gcc $(obj) -o $(target)

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

# 添加规则,删除生成的目标文件和可执行程序
# 这个规则比较特殊,clean 根本不会生成,这是一个伪目标
.PHONY: clean
clean:
rm $(obj) $(target)

练习题 1

如果觉得上边讲的内容看懂了,可以试着根据这个目录结构写出其对应的 Makefile 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 目录结构
.
├── include
│   └── head.h ==> 头文件,声明了加减乘除四个函数
├── main.c ==> 测试程序,调用了 head.h 中的函数
├── Makefile
└── src
├── add.c ==> 加法运算
├── div.c ==> 除法运算
├── mult.c ==> 乘法运算
└── sub.c ==> 减法运算

2 directories, 7 files

根据上边的项目目录结构编写的 Makefile 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 搜索多个指定目录下的.c 源文件
src = $(wildcard *.c ./src/*.c)
# 将上述源文件替换为以.o 为后缀的目标文件
obj = $(patsubst %.c, %.o, $(src))

include = ./include
target = calc

# 将所有不重复的依赖($^)链接生成目标($@):从目标文件生成可执行文件
$(target): $(obj)
gcc $^ -o $@

# 编译规则:从源文件生成目标文件
%.o: %.c
gcc $< -c -I$(include) -o $@

.PHONY: clean

clean:
rm -f $(obj) $(target)

编译过程日志:

1
2
3
4
5
6
7
8
9
10
11
12
$ make
gcc main.c -c -I./include -o main.o
gcc src/add.c -c -I./include -o src/add.o
gcc src/div.c -c -I./include -o src/div.o
gcc src/mult.c -c -I./include -o src/mult.o
gcc src/sub.c -c -I./include -o src/sub.o
gcc main.o src/add.o src/div.o src/mult.o src/sub.o -o calc
$ make
make: “calc”已是最新。
$ make clean
rm -f main.o ./src/add.o ./src/div.o ./src/mult.o ./src/sub.o calc
$

执行 make 后的项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── calc
├── include
│   └── head.h
├── main.c
├── main.o
├── Makefile
└── src
├── add.c
├── add.o
├── div.c
├── div.o
├── mult.c
├── mult.o
├── sub.c
└── sub.o

2 directories, 13 files

练习题 2

如果觉得上边讲的内容看懂了,可以试着根据这个目录结构写出其对应的 Makefile 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── htdocs
│   ├── check.cgi
│   ├── color.c
│   ├── color.cgi
│   ├── index.html
│   └── README
├── httpd.c
├── LICENSE
├── Makefile
├── README.md
└── simpleclient.c

1 directory, 10 files

根据上边的项目目录结构编写的 Makefile 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CC = gcc
WARNFLAGS = -W -Wall
LIBS = -lpthread #-lsocket

SRC = $(wildcard *.c ./htdocs/*.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
# 提取 SRC 中的文件名(包含目录)作为 EXEC 变量的值
EXEC := $(patsubst %.c, %, $(SRC)) # $(notdir $(SRC))

all: $(EXEC)

# 目标依赖于同名的 .o 文件
$(EXEC): %: %.o
$(CC) -g $(WARNFLAGS) $(LIBS) $< -o $@

# 编译规则:从源文件生成目标文件
%.o: %.c
$(CC) $(WARNFLAGS) -c $< -o $@

clean:
rm -f $(EXEC) $(OBJ)

编译过程日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ make
gcc -W -Wall -c httpd.c -o httpd.o
gcc -g -W -Wall -lpthread httpd.o -o httpd
gcc -W -Wall -c simpleclient.c -o simpleclient.o
simpleclient.c: In function ‘main’:
simpleclient.c:9:14: warning: unused parameter ‘argc’ [-Wunused-parameter]
9 | int main(int argc, char *argv[])
| ~~~~^~~~
simpleclient.c:9:26: warning: unused parameter ‘argv’ [-Wunused-parameter]
9 | int main(int argc, char *argv[])
| ~~~~~~^~~~~~
gcc -g -W -Wall -lpthread simpleclient.o -o simpleclient
gcc -W -Wall -c htdocs/color.c -o htdocs/color.o
gcc -g -W -Wall -lpthread htdocs/color.o -o htdocs/color
$ make
make: 对“all”无需做任何事。
$ make clean
rm -f httpd simpleclient ./htdocs/color httpd.o simpleclient.o ./htdocs/color.o
$

执行 make 后的项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── htdocs
│   ├── check.cgi
│   ├── color
│   ├── color.c
│   ├── color.cgi
│   ├── color.o
│   ├── index.html
│   └── README
├── httpd
├── httpd.c
├── httpd.o
├── LICENSE
├── Makefile
├── README.md
├── simpleclient
├── simpleclient.c
└── simpleclient.o

1 directory, 16 files

参考资料:

  1. 本文转载并修改自:https://subingwen.cn/linux/makefile/