Makefile(3)进阶

简介: Makefile(3)进阶

Makefile进阶

1.创建目录

毫⽆疑问,我们在编译项⽬之前希望⽤于存放⽂件的⽬录先准备好,当然,我们可以在编译之前通过⼿动

来创建所需的⽬录,但这⾥我们希望采⽤⾃动的⽅式。makefile的依赖树的样子是这样的。

这个依赖图从概念上说来是对的,但从 Makefile 的实现上存在⼀些问题。我们说 all 是⼀个⽬标,如果 all 直接依赖 objs 和 exes ⽬录的话,那应该如何创建⽬录呢?首先写一个makefile【注意代码的最后一行不能换行,表示一个依赖】

.PHONY:all
MKDIR=mkdir
DIRS=objs exes
all:$(DIRS)

运行结果

改进依赖关系图

改进上面的makefile

.PHONY:all
MKDIR=mkdir
RM=rm
DIRS=objs exes
all:$(DIRS)
$(DIRS):
    $(MKDIR) $@

运行结果

在这个 Makefile 中,需要注意的是 OBJS 变量即是⼀个依赖⽬标也是⼀个⽬录,在不同的场合其意思是不同的。⽐如,第⼀次 make 时,由于 objs 和 exes ⽬录都不存在,所以 all ⽬标将它们视作是⼀个先决条件或者说是依赖⽬标,接着 Makefile 先根据⽬录构建规则构建 objs 和 exes ⽬标,即Makefile 中的第⼆条规则就被派上了⽤场。构建⽬录时,第⼆条规则中的命令被执⾏,即真正的创建了 objs 和 exes ⽬录。当我们第⼆次进⾏ make 时,此时,make 仍以 objs 和 exes 为⽬标,但从⽬录构建规则中发现,这两个⽬标并没有依赖关系,⽽且能从当前⽬录中找到 objs 和 exes ⽬录,即认为 objs 和 exes ⽬标都是最新的,所以不⽤再运⾏⽬录构建规则中的命令来创建⽬录。

更新后代码的依赖树关系

接下来也得为 Makefile 创建⼀个 clean ⽬标,专⻔⽤来删除所⽣成的⽬标⽂件和可执⾏⽂件。加 clean 规则还是相当简单,需要再增加了两个变量,⼀个是RM,另⼀个则是 RMFLAGS。

.PHONY:all
MKDIR=mkdir
DIRS=objs exes
RM=rm
RMFLAGS=-fr
all:$(DIRS)
$(DIRS):
    $(MKDIR) $@
clean:
    $(RM) $(RMFLAGS) $(DIRS)

运行结果

2.增加头文件

.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
EXE=test
DIRS=objs exes
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
all:$(DIRS) $(EXE)
$(DIRS):
    $(MKDIR) $@
$(EXE):$(OBJS)
    $(CC) -o $@ $^
%.o:%.c
    $(CC) -o $@ -c $^
clean:
    $(RM) $(RMFLAGS) $(DIRS) $(EXE) $(OBJS)

运行结果

3.将文件放进目录

为了将⽬标⽂件或是可执⾏程序分别放⼊所创建的 objs 和 exes ⽬录中,我们需要⽤到 Makefile中的⼀

个函数 —— addprefix。对上面的makefile进行修改。

.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
 
DIR_OBJS=objs
DIR_EXE=exes
DIRS=$(DIR_OBJS) $(DIR_EXE)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
 
all:$(DIRS) $(EXE)
 
$(DIRS):
    $(MKDIR) $@
$(EXE):$(OBJS)
    $(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
    $(CC) -o $@ -c $^
clean:
    $(RM) $(RMFLAGS) $(DIRS)

运行结果

最⼤的变化除了增加了对于 addprefix 函数的运⽤为每⼀个⽬标⽂件加上“objs/”前缀外,还有⼀个很⼤的变化是,我们需要在构建⽬标⽂件的模式规则中的⽬标前也加上“objs/”前缀,即增加“$(DIR_OBJS)/”前缀。之所以要加上,是因为规则的命令中的-o 选项需要以它作为⽬标⽂件的最终⽣成位置,还有就是因为 OBJS 也加上了前缀,⽽要使得 Makefile 中的⽬标创建规则被运⽤,也需要采⽤相类似的格式,即前⾯有“objs/”。此外,由于改动后的 Makefile 会将所有的⽬标⽂件放⼊ objs ⽬录当中,⽽我们的 clean 规则中的命令包含将 objs ⽬录删除的操作,所以我们可以去除命令中对 OBJS 中⽂件的删除。这导致的改动就是 Makefile 中的最后⼀⾏中删除了$(OBJS)。同样的方法将 test 放入到 exes 文件夹中。

4.更复杂的依赖关系

假设我们对项⽬已经进⾏了⼀次编译(这⼀点⾮常重要,否则你看不到将要说的问题),接着对 foo.h⽂件进⾏了修改,如下

// foo.h
#ifndef __FOO_H
#define __FOO_H
void foo(int value);
#endif

并不对 foo.c进⾏相应的更改。这样⼀改的话,由于声名与定义不相同,所以理论上编译时应当出错。【先不要进行make clean,直接进行make,否则可能出现的结果和我们预期的不一致】

运行结果

通过运行结果可以看到,第一次make的时候并没有报错,但是如果我们事先做了make clean,这个报错信息又会出现,但是我们在正常的工程编译中不能不停的进行make clean,这样太费时。

分析一下第一次make出现异常的原因,下图是makefile所表达的依赖关系书以及规则的映射关系图。

从依赖关系图中可以发现,其中并没有出现对 foo.h 的依赖关系,这就是为什么我们改动头⽂件时,make ⽆法生效的原因!所以我们需要在构建目标的规则中增加对于foo.h的依赖。这里我们需要使用自动变量 $< 。这个变量与$的区别是,其只表示所有的先决条件中的第⼀个,⽽$则表示全部先决条件。之所以要⽤$<是因为,我们不希望将 foo.h 也作为⼀个⽂件让 gcc 去编译,这样的话会出错。

.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
 
DIR_OBJS=objs
DIR_EXE=exes
DIRS=$(DIR_OBJS) $(DIR_EXE)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
 
all:$(DIRS) $(EXE)
 
$(DIRS):
    $(MKDIR) $@
$(EXE):$(OBJS)
    $(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c foo.h
    $(CC) -o $@ -c $<
clean:
    $(RM) $(RMFLAGS) $(DIRS)

现在,将 foo.h 改回以前的状态,即去除 foo ()函数中的 int 参数,然后编译,这次编译当然是成功的,

接着再加⼊ int 参数,再编译。你发现这次真的能发现问题了!更改后的依赖关系图如下图

运行结果

当项⽬复杂时,如果我们要将每⼀个头⽂件都写⼊到 Makefile 相对应的规则中,这将会是⼀个恶梦!所以我们需要找到另⼀种更好的⽅法。

如果有哪⼀个⼯具能帮助我们列出⼀个源程序所包含的头⽂件那就好了,这样的话我们或许可以在 make

时,动态的⽣成⽂件的依赖关系。还真是存在这么⼀个⼯具!就是我们的编译器 ——gcc。下图列出了采⽤ gcc 的-M 选项和-MM 选项列出 foo.c 对其它⽂件的依赖关系的结果,从结果你可以看出它们会列出 foo.c 中直接或是间接包含的头⽂件。-MM 选项与-M 选项的区别是,-MM选项并不列出对于系统头⽂件的依赖关系,⽐如 stdio.h 就属于系统头⽂件。其道理是,绝⼤多数情况我们并不会改变系统的头⽂件,⽽只会对⾃⼰项⽬的头⽂件进⾏更改。

对于采⽤ gcc 的-MM 的选项所⽣成的结果,还有⼀个问题,因为我们⽣成的⽬标⽂件是放在 objs⽬录当

中的,因此,我们希望依赖关系中也包含这⼀⽬录信息,否则,在我们的 Makefile 中,跟本没有办法做

到将⽣成的⽬标⽂件放到 objs ⽬录中去,这在前⾯的 Makefile 中我们就是这么做的。在使⽤新的⽅法

时,我们仍然需要实现同样的功能。这时,我们需要⽤到 sed ⼯具了,这是 Linux 中⾮常常⽤的⼀个字符

串处理⼯具。下图是采⽤ sed 进⾏查找和替换以后的输出结果,从结果中我们可以看到,就是在 foo.o 之前加上了“objs/”前缀。对于 sed 的⽤法说明可能超出了本⽂的范围,如果你不熟悉其功能,可以找⼀些资料看⼀看。

gcc -MM foo.c | sed 's,..∗\.o[ :]*,objs/\1.o: ,g'

执行结果

gcc 还有⼀个⾮常有⽤的选项是-E,这个命令告诉 gcc 只做预处理,⽽不进⾏程序编译,在⽣成依赖关

系时,其实我们并不需要 gcc 去编译,只要进⾏预处理就⾏了。这可以避免在⽣成依赖关系时出现没有必

要的 warning,以及提⾼依赖关系的⽣成效率。

现在,我们已经有了⾃动⽣成依赖关系的⽅法了,那如何将其整合到我们的 Makefile 中呢?显然,⾃动

⽣成的依赖信息,不可能直接出现在我们的 Makefile 中,因为我们不能动态的改变 Makefile中的内容,

那采⽤什么⽅法呢?先别急,第⼀步我们能做的是,为每⼀个源⽂件通过采⽤ gcc 和 sed⽣成⼀个依赖关

系⽂件,这些⽂件我们采⽤.dep 后缀结尾。从模块化的⻆度来说,我们不希望.dep⽂件与.o ⽂件或是可

执⾏⽂件混放在⼀个⽬录中。为此,创建⼀个新的 deps ⽬录⽤于存放依赖⽂件似乎更为合理。

1. .PHONY:all clean
2. MKDIR=mkdir
3. RM=rm
4. RMFLAGS=-fr
5. CC=gcc
6. 
7. DIR_OBJS=objs
8. DIR_EXE=exes
9. DIR_DEPS=deps
10. DIRS=$(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)
11. EXE=test
12. EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
13. SRCS=$(wildcard *.c)
14. OBJS=$(SRCS:.c=.o)
15. OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
16. DEPS = $(SRCS:.c=.dep)
17. DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
18. 
19. all:$(DIRS) $(DEPS) $(EXE)
20. 
21. $(DIRS):
22.     $(MKDIR) $@
23. $(EXE):$(OBJS)
24.     $(CC) -o $@ $^
25. $(DIR_OBJS)/%.o:%.c
26.     $(CC) -o $@ -c $^
27. $(DIR_DEPS)/%.dep: %.c
28.     @echo "Making $@ ..."
29.     @set -e; \
30.     $(RM) $(RMFLAGS) $@.tmp ; \
31.     $(CC) -E -MM $^ > $@.tmp ; \
32.     sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
33.     $(RM) $(RMFLAGS) $@.tmp
34. clean:
35.     $(RM) $(RMFLAGS) $(DIRS)

运行结果:

5.包含文件

现在依赖⽂件已经有了,那如何为我们的 Makefile 所⽤呢?这需要⽤到 Makefile 中的 include关键字,

它如同 C/C++中的#include 预处理指令。现在要做的就是在 Makefile 中加⼊对所有依赖⽂件的包含功

能,更改后的 Makefile 如下所示。

.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
 
DIR_OBJS=objs
DIR_EXE=exes
DIR_DEPS=deps
DIRS=$(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
 
all:$(DIRS) $(DEPS) $(EXE)
 
$(DIRS):
    $(MKDIR) $@
$(EXE):$(OBJS)
    $(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
    $(CC) -o $@ -c $^
$(DIR_DEPS)/%.dep: %.c
    @echo "Making $@ ..."
    @set -e; \
    $(RM) $(RMFLAGS) $@.tmp ; \
    $(CC) -E -MM $^ > $@.tmp ; \
    sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
    $(RM) $(RMFLAGS) $@.tmp
clean:
    $(RM) $(RMFLAGS) $(DIRS)

运行结果

从上图可以看到,由于 make 在处理Makefile 的 include 命令时,发现找不到 deps/foo.dep 和 deps/main.dep,所以出错了。如何理解这⼀错误呢?从这⼀错误我们可知,make 对于 include 的处理是先于 all ⽬标的构建的,这样的话,由于依赖⽂件是在构建 all ⽬标时才创建的,所以很⾃然 make 在处理 include 指令时,是找不到依赖⽂件的。我们说第⼀次 make 的确没有依赖⽂件,所以 include 出错也是正常的,那能不能让 make忽略这⼀错误呢?可以的,在 Makefile 中,如果在 include 前加上⼀个‘-’号,当 make 处理这⼀包含指示时,如果⽂件不存在就会忽略这⼀错误。除此之外,需要对于 Makefile 中的 include 有更为深⼊的了解。当 make 看到 include 指令时,会先找⼀下有没有这个⽂件,如果有则读⼊。接着,make 还会看⼀看对于包含进来的⽂件,在 Makefile 中是否存在规则来更新它。如果存在,则运⾏规则去更新需被包含进来的⽂件,当更新完了之后再将其包含进来。在我们的这个 Makefile 中,的确存在⽤于创建(或更新)依赖⽂件的规则。那为什么 make 没有帮助我们去创建依赖⽂件,⽽只是抱怨呢?因为 make 想创建依赖⽂件时,deps ⽬录还没有创建,所以⽆法成功的构建依赖⽂件。

有了这些信息之后,我们需要对 Makefile 的依赖关系进⾏调整,即将 deps ⽬录的创建放在构建依赖⽂

件之前。其改动就是在依赖⽂件的创建规则当中增加对 deps ⽬录的信赖,且将其当作是第⼀个先决条

件。

采⽤同样的⽅法,我们将所有的⽬录创建都放到相应的规则中去。更改后的 Makefile如下所示。

.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
 
DIR_OBJS=objs
DIR_EXE=exes
DIR_DEPS=deps
DIRS=$(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS=$(SRCS:.c=.dep)
DEPS:=$(addprefix $(DIR_DEPS)/, $(DEPS))
 
all: $(EXE)
 
-include $(DEPS)
 
$(DIRS):
    $(MKDIR) $@
$(EXE):$(DIR_EXE) $(OBJS)
    $(CC) -o $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o:$(DIR_OBJS) %.c
    $(CC) -o $@ -c $(filter %.c, $^)
$(DIR_DEPS)/%.dep:$(DIR_DEPS) %.c
    @echo "Making $@ ..."
    @set -e; \
    $(RM) $(RMFLAGS) $@.tmp ; \
    $(CC) -E -MM $(filter %.c, $^) > $@.tmp ; \
    sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
    $(RM) $(RMFLAGS) $@.tmp
clean:
    $(RM) $(RMFLAGS) $(DIRS)

我们使⽤了 filter 函数将所依赖的⽬录从先决条件中去除,否则的话会出现错误。正如前⾯所提及的,当make 看到 include 指令时,会试图去构建所需包含进来的依赖⽂件,这样⼀来,我们并不需要让 all⽬录依赖依赖⽂件,也就是从 all 规则中去除了对 DEPS 的依赖。

6.条件语法

当 make 看到条件语法时将⽴即对其进⾏分析,这包括 ifdef、ifeq、ifndef 和 ifneq 四种语句形式。这

也说明⾃动变量(参⻅ 1.5.1 节)在这些语句块中不能使⽤,因为⾃动变量的值是在命令处理阶段才被赋

值的。如果⾮得⽤条件语法,那得使⽤ Shell 所提供的条件语法⽽不是 Makefile 的。Makefile 中的条件语法有三种形式,如下所示。其中的 conditional-directive 可以是 ifdef、ifeq、ifndef 和 ifneq 中的任意⼀个。

conditional-directive
    text-if-true
endif
conditional-directive
    text-if-true
else
    text-if-false
endif
conditional-directive
    text-if-one-is-true
else conditional-directive
    text-if-true
else
    text-if-false
endif
.PHONY: all
sharp = square
desk = square
table = circle
 
ifeq ($(sharp), $(desk))
    result1 = "desk == sharp"
endif
ifneq "$(table)" 'square'
    result2 = "table != square"
endif
all:
    @echo $(result1)
    @echo $(result2)

运行结果

ifdef和ifndefg示例

.PHONY: all
foo = defined
ifdef foo
    result1 = "foo is defined"
endif
ifndef bar
    result2 = "bar is not defined"
endif
all:
    @echo $(result1)
    @echo $(result2)

运行结果

文章知识点与官方知识档案匹配,可进一步学习相关知识

C技能树进阶任务C语言问答203399 人正在系统学习中

关注博

相关文章
|
6月前
|
Shell Linux C++
Makefile编译实战
Makefile编译实战
82 0
|
Java 编译器 Linux
Makefile教程(入门介绍)
Makefile教程(入门介绍)
113 0
|
3月前
|
编译器 Linux C语言
Makefile实战论(一)
Makefile实战论(一)
|
2月前
|
Shell
Makefile 基本介绍
Makefile 是一种用于自动化构建过程的脚本文件,通过文件的时间戳来判断文件是否需要重新编译,从而提高编译效率。它由一系列规则组成,每个规则包含目标、依赖和命令三部分。Makefile 支持多种语法结构,如变量、条件语句和伪目标。`make` 命令则用于执行 Makefile 中定义的任务,如编译源代码、生成可执行文件等。通过定义变量和特殊符号,Makefile 能够灵活地管理复杂的工程项目。
|
6月前
Makefile(1)入门
Makefile(1)入门
44 0
|
6月前
|
存储 编译器 Shell
Makefile语法基础
Makefile语法基础
26 0
|
6月前
|
编译器 Shell C语言
Makefile快速入门
Makefile快速入门
47 0
|
11月前
|
Shell
Makefile学习2
Makefile学习2
|
11月前
|
编译器 Shell Linux
Makefile学习1
Makefile学习1
|
Java Linux 程序员
玩转Makefile | 一文入门Makefile
玩转Makefile | 一文入门Makefile
239 0
玩转Makefile | 一文入门Makefile