不同场景下的 Makefile

姊妹篇 《Makefile 入门》 中已经介绍过隐含规则、自动推导。要想在 make & Makefile 上精进,必须了解其隐含规则。它可以让你省去很多繁琐、重复的细节,快速高效地完成项目的编译和链接。

隐含规则依赖同名(同名源文件-同名目标文件-同名可执行文件),一般而言设置好编译器属性、给出必要的头文件目录,就可以使用隐含规则生成同名的目标文件(.o 文件);但实际项目中很少出现可执行文件由单独一个源文件/目标文件生成的情况,所以生成可执行文件(包括链接库)时一般不能只使用隐含规则达到目的。

只有一个源文件

假设目录下只有 test.c 一个源文件。

最简单的方式:(只使用隐含规则)

不写 Makefile 文件,直接执行 make test。可以看到: make(其实是默认生成的 Makefile 文件) 默认使用 cc 编译器(不是 gcc)。

1
2
3
4
5
vimer@debian8light:~/see-the-world/code/make_learn$ ls
test.c
vimer@debian8light:~/see-the-world/code/make_learn$ make test
cc test.c -o test ## Makefile 隐含规则生成的 gcc 执行语句,可以看到
# Makefile 倾向的书写方式

当目录下存在 test.o 文件时直接执行 make test 命令

1
2
3
4
5
vimer@debian8light:~/see-the-world/code/make_learn$ ls
Makefile.bak test.c test.o
vimer@debian8light:~/see-the-world/code/make_learn$ make test
cc test.o -o test
vimer@debian8light:~/see-the-world/code/make_learn$

可以看到 Makefile 隐含规则在生成可执行文件时倾向于将可执行文件放在命令的最后,即 ${CC} $^ -o $@ 的形式。

扩展:我们看到上文中使用的都是 cc 编译器,我们更习惯 gcc/g++ 对不?而且有时候还需要给编译器指定参数,比如使用 c99 标准,怎么做呢?很简单,只需要在当前目录下新建以下 Makefile 文件:

1
2
3
4
5
6
# 指定编译器和选项
CC=gcc
CFLAGS=-Wall -std=c99

CXX=g++
CXXFLAGS=-Wall -std=c++11

再次执行 make test 看看效果吧

1
2
3
4
5
6
7
vimer@debian8light:~/see-the-world/code/make_learn$ ls
Makefile Makefile.bak test.c text.cpp
vimer@debian8light:~/see-the-world/code/make_learn$ make test
gcc -Wall -std=c99 test.c -o test
vimer@debian8light:~/see-the-world/code/make_learn$ make text
g++ -Wall -std=c++11 text.cpp -o text
vimer@debian8light:~/see-the-world/code/make_learn$

可以看到:Makefile 隐含规则会自动使用 CCCFLAGS 变量,针对 .cpp 文件则自动使用 CXXCXXFLAGS 变量。

也可以看出,直接使用源文件生成可执行文件时,更准确的格式是 ${CC} ${CFLAGS} $^ -o $@

最简单的 Makefile 文件:

其实最简单的 Makefile 文件就是没有 Makefile文件,其次就是上一节中“只指定编译器及选项”的 Makefile 文件。但也由于其太过简单,在实际生产中并不具备实用性。下面来看一个“麻雀虽小,五脏俱全”的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 可执行文件
TARGET=test
# 依赖目标
OBJS= test.o

# 指定编译器和选项
CC=gcc
CFLAGS=-Wall -std=c99

.PHONEY:all clean

all:${TARGET}

# 目标文件生成可执行文件
${TARGET}:${OBJS}
${CC} $^ -o $@

# 使用隐含规则生成目标文件

clean:
${RM} ${TARGET} ${OBJS}

执行 make 命令。可以看到:Makefile 隐含规则在生成目标文件时倾向于将源文件放在命令的最后,即 ${CC} ${CLFAGS} -c -o $@ $< 的形式。

1
2
3
vimer@debian8light:~/see-the-world/code/make_learn$ make
gcc -Wall -std=c99 -c -o test.o test.c ## Makefile 倾向的书写方式
gcc test.o -o test

多个源文件

比如说三个:test.c test-add.c test-sub.c。这种情况下,我们只需要将上述 Makefile 文件中的“依赖目标”稍作修改即可(修改后的 Makefile 依旧适用之前的案例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 可执行文件
TARGET=test
# 扫描源文件
SRCS=${wildcard *.c}
#SRCS=test.c test-add.c test-sub.c
# 依赖目标
OBJS=${SRCS:.c=.o}

# 指定编译器和选项
CC=gcc
CFLAGS=-Wall -std=c99

.PHONEY:all clean

all:${TARGET}

# 目标文件生成可执行文件
${TARGET}:${OBJS}
${CC} $^ -o $@

# 使用其隐含规则生成目标文件

clean:
${RM} ${TARGET} ${OBJS}

make 执行结果:

1
2
3
4
5
6
7
8
vimer@debian8light:~/see-the-world/code/make_learn$ ls
Makefile test-add.c test.c test-sub.c
vimer@debian8light:~/see-the-world/code/make_learn$ make
gcc -Wall -std=c99 -c -o test-add.o test-add.c
gcc -Wall -std=c99 -c -o test-sub.o test-sub.c
gcc -Wall -std=c99 -c -o test.o test.c
gcc test-add.o test-sub.o test.o -o test
vimer@debian8light:~/see-the-world/code/make_learn$

目录分级 & 头文件

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
vimer@debian8light:~/see-the-world/code/make_learn$ tree
.
├── test-add
│ ├── test-add.c
│ └── test-add.h
├── test.c
└── test-sub
├── test-sub.c
└── test-sub.h

2 directories, 5 files
vimer@debian8light:~/see-the-world/code/make_learn$

其中 test.c 文件中如下引入头文件:

1
2
3
#include <stdio.h>
#include "test-add.h"
#include "test-sub.h"

预处理错误

如果我们直接 make test 那么一定会报错 test.c:2:22: fatal error: test-add.h: 没有那个文件或目录 找不到头文件。我们可以通过以下方式解决这个错误:

  1. make 命令中指定参数:make test CPPFLAGS='-Itest-add -Itest-sub',因为参数中包含空格,所以必须用引号括起来。此时使用的仍然是 cc 编译器,我们可以在命令行中指定 CC=gcc 参数等等。

  2. 使用 Makefile 文件:我们延用第一节中的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 指定编译器和选项
    CC=gcc
    CFLAGS=-Wall -std=c99

    CXX=g++
    CXXFLAGS=-Wall -std=c++11

    # 给预处理器传参
    CPPFLAGS=-I'./test-add' -I'./test-sub'
    # CPPFLAGS=-I./test-add -I./test-sub # 这么写也可以
    # CPPFLAGS='-I./test-add -I./test-sub' # 这么写报错,为什么呢?

上述的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
vimer@debian8light:~/see-the-world/code/make_learn$ ls
Makefile test-add test.c test-sub
vimer@debian8light:~/see-the-world/code/make_learn$ make test
gcc -Wall -std=c99 -I'./test-add' -I'./test-sub' test.c -o test
(作者备注:以下报错链接错误)
/tmp/ccQlkvYC.o:在函数‘main’中:
test.c:(.text+0x49):对‘add’未定义的引用
test.c:(.text+0x69):对‘sub’未定义的引用
collect2: error: ld returned 1 exit status
<builtin>: recipe for target 'test' failed
make: *** [test] Error 1
vimer@debian8light:~/see-the-world/code/make_learn$

链接错误

我们使用第二节中的 Makefile,对“源文件”做相应修改就可:(当然也需要新添 CPPFLAGS 变量)

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
# 可执行文件
TARGET=test
# 扫描源文件
SRCS=test.c \
test-add/test-add.c \
test-sub/test-sub.c
# 依赖目标
OBJS=${SRCS:.c=.o}

# 指定编译器和选项
CC=gcc
CFLAGS=-Wall -std=c99
CPPFLAGS=-I'./test-add' -I'./test-sub'

.PHONEY:all clean

all:${TARGET}

# 目标文件生成可执行文件
${TARGET}:${OBJS}
${CC} $^ -o $@

# 使用其隐含规则生成目标文件

clean:
${RM} ${TARGET} ${OBJS}

成功执行:

1
2
3
4
5
6
vimer@debian8light:~/see-the-world/code/make_learn$ make
gcc -Wall -std=c99 -I'./test-add' -I'./test-sub' -c -o test.o test.c
gcc -Wall -std=c99 -I'./test-add' -I'./test-sub' -c -o test-add/test-add.o test-add/test-add.c
gcc -Wall -std=c99 -I'./test-add' -I'./test-sub' -c -o test-sub/test-sub.o test-sub/test-sub.c
gcc test.o test-add/test-add.o test-sub/test-sub.o -o test
vimer@debian8light:~/see-the-world/code/make_learn$

共享库

系统共享库

如果使用到系统共享库又该怎么做呢?我们知道共享库是用在链接阶段的,参考第一节,在隐含规则中 CPPFLAGS 是传给预处理器的,CFLAGS 是传给编译器的,相应的传给链接器的变量是 LDLIBS

假设 test.c 源文件中调用了 #include <math.h>sin() 函数,那么只需要在链接时指定 LDLIBS=-lm 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vimer@debian8light:~/see-the-world/code/make_learn$ ls
test.c
vimer@debian8light:~/see-the-world/code/make_learn$ make test.o # 编译阶段不需要任何特殊处理
cc -c -o test.o test.c
vimer@debian8light:~/see-the-world/code/make_learn$ make test # 报链接错误
cc test.o -o test
test.o:在函数‘main’中:
test.c:(.text+0x34):对‘sin’未定义的引用
collect2: error: ld returned 1 exit status
<builtin>: recipe for target 'test' failed
make: *** [test] Error 1
vimer@debian8light:~/see-the-world/code/make_learn$ make test LDLIBS=-lm # 使用 libm.so 库
cc test.o -lm -o test
vimer@debian8light:~/see-the-world/code/make_learn$ ls
source_learn test test.c test.o
vimer@debian8light:~/see-the-world/code/make_learn$ ./test
sin(30.00)=0.50
vimer@debian8light:~/see-the-world/code/make_learn$

自定义共享库

在使用 make & Makefile 文件生成可执行文件过程中,使用系统共享库和自定义共享库的区别在于:后者需要使用 LDFLAGS 变量指定路径。

运行可执行文件又会有一些区别,关于链接库更多的知识请移步 《共享库 & 静态库》

参考

整篇笔记的结构以及用到源码参考自 例说makefile。但此系列笔记有两个不足之处:

  1. 原文中的 DLIBS INC 其实就是 LDLIBSCPPFLAGS 变量。

    • 虽说如果不使用隐含规则,只是显式地使用,这些变量随便起什么名字都可以;
    • 但既然使用 Makefile & make,那么完全放弃使用隐含规则有“大器小用”之嫌,和单纯使用 shell 脚本还有区别吗?
    • 使用 Makefile 的隐含规则,就应该使用 其隐含规则用到的变量(限于 GNU make);
    • Why LD_LIBRARY_PATH is bad 中提到的 LD_RUN_PATH 变量,在隐含规则中是否生效?
    • DLIBS INC 是其他 make (非 GNU make)的预定义变量吗?
  2. 原文中讲到自定义共享库时并没有细致划分 soname、linker name、real name。这个只是小瑕疵