Linux基础——Makefile编写优化

本文主要是为了讲述如何能够编写更专业的Makefile,而不是仅仅通过一项简单的依赖规则生成几个固定目标。(重点参考李云老师编写的《专业嵌入式软件开发——全名走向高质量高效编程》一书,该书是我在图书馆偶然看到,发现原来长期一直使用的Makefile中居然还有这么多需要注意的细节,所以特地整理记录下来。)


Makefile,开发环境全能管家

Linux 环境下的程序员如果不会使用GNU make来构建和管理自己的工程,应该不能算是一个合格的专业程序员,至少不能称得上是 Unix程序员。在 Linux(unix )环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为Makefile 文件的编写。
所要完成的Makefile 文件描述了整个工程的编译、连接等规则。其中包括:工程中的哪些源文件需要编译以及如何编译、需要创建哪些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。尽管看起来可能是很复杂的事情,但是为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。
make是一个命令工具,它解释Makefile 中的指令。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile在绝大多数的IDE 开发环境中都在使用,已经成为一种工程的编译方法。

基本规则

基本规则
让我们先来粗略地看一看Makefile的规则。

target ... : prerequisites ...
	command
	...
	...

以上是Makefile的基本规则,解释如下:
目标:依赖
执行指令 …
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)。
① prerequisites就是,要生成那个target所需要的文件或是目标。
② command也就是make需要执行的命令。(任意的Shell命令)
这里关于最基本的Makefile就不详细解释了,执行make命令后Makefile会首先生成描述的第一个目标,查看依赖是否成立,如果依赖不存在再去依次生成依赖。。。。最终成功生成第一个目标。

假目标的用处

假目标采用.PHONY关键字来定义,注意它必须是大写字母,通常最基本会将clean修改为假目标,这样避免了当前文件下存在“clean”同名的文件导致make执行出现偏差。如下:

.PHONY: clean
simple: main.o foo.o
	gcc -o simple main.o foo.o
main.o : main.c
	gcc -o main.o -c main.c
foo.o : foo.c
	gcc -o foo.o -c foo.c

clean:
	rm -rf simple main.o foo.o
  • 由于假目标并不会与文件相关联,所以每次构建假目标时它所在的规则中的命令都一定会被执行,也就是上面每次执行“make clean”都会执行文件清除操作。

运用“变量”提高Makefile可维护性

编写专业的Makefile同样离不开运用变量,通过使用变量可以使得Makefile更具有可维护性。如下:

.PHONY:clean

CC = gcc
RM = rm

EXE = simple
OBJS = main.o foo.o

$(EXE) : $(OBJS)
	$(CC) -o $(EXE) $(OBJS)
main.o : main.c
	$(CC) -o main.o -c main.c
foo.o : foo.c
	$(CC) -o foo.o -c foo.c

clean:
	$(RM) -rf $(EXE) $(OBJS)

在这里插入图片描述

  • 这里,我们定义了CC,RM,EXE,OBJS四个变量,定义变量时其值可以为空(即无右值),引用变量需要采用“ ( ) (变量名)”或者“ {变量名}”的形式。
  • 引用变量的好处很明显,比如进入CC变量以后,如果需要修改编译器,只需要修改赋值变量这一处即可。Makefile中变量的数据类型可以理解为C语言中的字符串。

自动变量

在上面的Makefile中,存在目标名和先决条件在规则的命令中重复出现。如果目标名或者先决条件名发生改变,那得在相应的命令中都去修改,这很麻烦,为了省去这种麻烦,我们可以借助以下自动变量。

  • $@:用于表示一个规则中的目标,当一个规则中有多个目标时,指其中任何造成规则命令被运行的目标。
  • $^:表示的是规则中的所有先决条件。
  • $<:表示的是规则中的第一个先决条件。
.PHONY : all
all: first second third
	@echo "\$$@ = $@"
	@echo "$$^ = $^"
	@echo "$$< = $<"

first second third:

在这里插入图片描述

除了这三个自动变量外,在Makefile中还可以使用其他的自动变量,后面我们会说到,以上三个自动变量是最常用的。使用上述自动变量来简化之前的Makefile:

.PHONY:clean

CC = gcc
RM = rm

EXE = simple
OBJS = main.o foo.o

$(EXE) : $(OBJS)
	$(CC) -o $@ $^
main.o : main.c
	$(CC) -o $@ -c $^
foo.o : foo.c
	$(CC) -o $@ -c $^

clean:
	$(RM) -rf $(EXE) $(OBJS)

特殊变量

在Makefile中,有两个特殊变量会经常用到:MAKE和MAKECMDGOALS。
MAKE:表示当前处理Makefile的命令名是什么。
MAKECMDGOALS:表示是是当前构建的目标名。
如下:

.PHONY = all clean
all clean:
	echo "MAKE = $(MAKE)"
	echo "MAKECMDGOALS = $(MAKECMDGOALS)"

在这里插入图片描述

  • 从结果上看,MAKECMDGOALS变量指的是用户输入的目标,当只允许make命令不带参数时,根据Makefile的语法规则会将Makefile中的第一个目标作为默认目标,即上面的all目标,但是MAKECMDGOALS却仍是空而不是“all”。
  • 另外,从结果中能看到,运行make时可以同时指定多个目标,make在获得了多个目标后,将从左往右依次地构建目标。

变量的类型与赋值

变量的类型有递归扩展变量和简单扩展变量。
最简单的方式是使用“=”进行变量的定义和赋值,这种只用一个“=”符号定义的变量被称为递归扩展变量。如下:

  • 递归扩展变量
.PHONY = all

foo = $(bar)
bar = $(ugh)
ugh = HUH?

all:
	@echo $(foo)

在这里插入图片描述

这种递归扩展变量最需要注意的一点就是要防止对变量进行循环扩展,容易造成一个死循环。

  • 简单扩展变量
    简单扩展变量是用 “ := ”操作符来定义的。这种变量,make只会对其进行一次展开,如下示例:
.PHONY = all

x = foo
y = $(x) b
x = later

xx := foo
yy := $(xx) b
xx := later

all:
	@echo "x = $(y), xx = $(yy)"

在这里插入图片描述

  • 另外,在Makefile中还可以实现条件赋值,“?=”操作符实现,当变量没有被定义时就定义它,并且将右边的值赋值给它;如果变量已经定义了,则不改变其原值。条件赋值操作可以用于为变量赋值默认值。
.PHONY = all

foo = x
foo ?= y
bar ?=y

all:
	@echo "foo=$(foo), bar=$(bar)"

在这里插入图片描述

  • 还有一个非常有用的赋值方法是通过“+=”实现追加赋值。
.PHONY:all

objects = main.o foo.o bar.o utils.o
objects += another.o

all:
	@echo $(objects)

在这里插入图片描述

变量及其值的来源

从前面的示例可以看出,在Makefile中可以对变量进行定义。此外,还有其他方式使得make获得变量。比如:

  • 对于自动变量,其值是在每一个规则中根据规则的上下文自动获得的。
  • 在运行make时,通过命令参数定义变量。例如下面的Makefile,如果使用“make bar=x”来运行它,得到结果就不一样了。
.PHONY : all

foo = x
foo ?= y
bar ?= y
all:
	@echo "foo = $(foo), bar = $(bar)"

在这里插入图片描述

  • 变量还可以来自shell环境,如下:
    在这里插入图片描述### 高级变量引用功能
    如下的Makefile说明了变量引用的一种高级功能,即在赋值的同时完成文件名后缀替换操作。
.PHONY = all
foo = a.c b.c c.c
bar := $(foo:.c=.o)

all:
	@echo "bar = $(bar)"

在这里插入图片描述
从截图中的运行结果来看,bar变量中的文件名由.c后缀变成了.o。与使用函数相比,这种方式根据简洁。当然,这种功能也可以采用后面将要介绍的patsubst函数来实现。

避免变量被覆盖的方法

上面介绍了make命令行上定义变量的方式能够使得Makefile文件中定义的变量值被覆盖。但是如果在设计Makefile时不希望发生这种覆盖现象,则需要使用override指令进行预防。具体如下:

.PHONY: all

override foo = x

all:
	@echo "foo = $(foo)"

在这里插入图片描述

借助“模式”精简规则

对于之前使用到的Makefile,其中存在多个规则用于构建目标文件。比如,main.o和foo.o,都是采用不同的规则进行描述,如果对于每个目标文件,都得写一个不同的规则来描述,那就变成了很麻烦的事了,虽然也能实现想要的效果。Makefile中的模式就是用来解决这种烦恼的。如下的Makefile就运用到了模式。

.PHONY: clean

CC=gcc
RM=rm

EXE=simple
OBJS=main.o foo.o

$(EXE) : $(OBJS)
	$(CC) -o $@ $^
%.o : %.c
	$(CC) -o $@ -c $^

clean:
	$(RM) -rf $(EXE) $(OBJS)

在这里插入图片描述

通过“函数”增强功能

函数是Makefile中的另一个离奇,通过使用函数能显著增强Makefile的功能。对于之前的项目的Makefile,尽管使用了模式规则,但还有一件比较麻烦的事——在Makefile中要指明每一个项目源文件。
如下是采用了wildcard和patsubst函数修改后的Makefile:

.PHONY: clean
CC = gcc
RM = rm

EXE = simple
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))

$(EXE) : $(OBJS)
	$(CC) -o $@ $^
%.o : %.c
	$(CC) -o $@ -c $^
clean:
	$(RM) -rf $(EXE) $(OBJS)

执行结果如下:
在这里插入图片描述当使用函数编辑Makefile后,在增加需要编译的源文件后不需要再修改Makefile,直接就能对新增的文件进行编译。如下:
在这里插入图片描述下面就来详细介绍一下Makefile中经常用到的几个函数。更多函数的使用方法可以参考官方手册《GNU Make》。

abspath函数

abspath函数被用于将_names中的各路径名转换成绝对路径,并将转换后的结果返回。其形式:

$(abspath _names)

如下示例:

.PHONY: all

ROOT := $(abspath /usr/../lib)

all:
	@echo $(ROOT)

执行结果如下:
在这里插入图片描述

addprefix函数

addprefix函数被用于给名字列表_names中的每一个名字增加前缀_prefix,并将增加了前缀的名字列表返回。其形式:

$(addprefix _prefix, _names)

如下示例使用:

.PHONY : all

without_dir = foo.c bar.c main.o
with_dir := $(addprefix objs/, $(without_dir))

all:
	@echo $(with_dir)

执行结果如下:
在这里插入图片描述

addsuffix函数

addsuffix函数被用于给名字列表_names中的每个名字增加后缀_suffix,并将增加了后缀_suffix的名字列表返回。其形式如下:

$(addsuffix _suffix, _names)

使用示例:

.PHONY : all

without_suffix = foo bar main
with_suffix := $(addsuffix .c, $(without_suffix))

all:
	@echo $(with_suffix)

执行如下:
在这里插入图片描述

eval函数

eval函数的存在使得Makefile具有动态语言的特征。eval函数使得make将再一次解析_text语句,eval函数返回值为空字符串,形式如下:

$(eval _text)

如下示例说明了它的用法:

.PHONY : all

sources = foo.c bar.c baz.s ugh.h
$(eval sources := $(filter %.c %.s, $(sources)))

all:
	@echo $(sources)

执行结果如下:
在这里插入图片描述

filter函数

filter函数被用于从一个 名字列表 _text中根据模式_pattern得到满足需要的名字列表并返回。其形式:

$(filter _pattern, _text)

如下示例使用方式:

.PHONY : all

sources = foo.c bar.c baz.s ugh.h
sources := $(filter %.c %.s, $(sources))

all:
	@echo $(sources)

执行结果如下:
在这里插入图片描述

filter-out函数

filter-out函数被用于从名字列表_text中根据模式_pattern滤除一部分名字,并将滤除后的列表返回。其形式如下:

$(filter-out _pattern, _text)

如下示例说明了它的用法:

.PHONY : all

objects = main1.o foo.o main2.o bar.o
result = $(filter-out main%.o, $(objects))

all:
	@echo $(result)

执行结果如下:
在这里插入图片描述

notdir函数

notdir函数被用来从路径_names中抽取文件名,并将文件名返回。其形式如下:

$(notdir _names)

如下示例说明了它的用法:

.PHONY : all
file_name := $(notdir code/foo/src/foo.c code/bar/src/bar.c)

all:
	@echo $(file_name)

执行结果:
在这里插入图片描述

patsubst函数

patsubst函数被用来将名字列表_text中符合_pattern模式的名字替换为_replacement,并将替换后的名字列表返回。其使用形式如下:

$(patsubst _pattern, _replacement, _text)

如下示例说明了它的使用方法,这里采用patsubst函数进行字符串替换,将所有.c结尾的名字替换为.o结尾的文件名。

.PHONY : all

mixed = foo.c bar.c main.o
objects := $(patsubst %.c, %.o, $(mixed)

all:
	@echo $(objects)

执行结果如下:
在这里插入图片描述#### realpath函数
realpath函数被用于获取_names所对应的真实路径名。其形式如下:

$(realpath _names)

如下示例说明了它的用法:

.PHONY : all

ROOT := $(realpath ./..)

all:
	@echo $(ROOT)

执行结果如下:
在这里插入图片描述

strip函数

如果希望清除名字列表中的多余空格,strip函数就是最好的选择。strip函数将_string中的多余空格去除后返回。形式如下:

$(strip _string)

示例使用如下:

.PHONY : all
original = foo.c bar.c
stripped := $(strip $(original))

all:
	@echo "original = $(original)"
	@echo "stripped = $(stripped)"

执行后结果如下:
在这里插入图片描述

wildcard函数

wildcard是通配符函数,通过它可以得到当前工作目录中满足_pattern模式的文件或目录名列表。形式如下:

$(wildcard _pattern)

如下示例说明了Makefile通过使用wildcard函数得到所以C源文件的名字列表。

.PHONY : all
SRCS = $(wildcard *.c)

all:
	@echo $(SRCS)

执行结果如下:
在这里插入图片描述

提高编译环境的实用性

之前的Makefile示例也只是为了介绍Makefile中可以用到的各种规则、变量、函数的使用方法。接下来,我们来构建一个略微复杂的虚拟项目——complicated项目,该项目中用到源代码如下:

  • foo.h
#ifndef _FOO_H
#define _FOO_H

void foo();

#endif
  • foo.c
#include <stdio.h>
#include "foo.h"

void foo()
{
	printf("this is foo()!\n");
}
  • main.c
#include "foo.h"
int main()
{
	foo();
	return 0;
}

让编译环境更加有序

大多的软件项目都会通过合理地设计目录结构来提高它的可维护性。在编译一个项目时会产生大量的中间文件,如果中间文件与项目的源程序文件直接混放在一起,就会显得混乱不堪而不利于维护。
这里,我们通过使用目录来使得编译环境更加有序。目录的引入并不是一步到位的,我们还将在后面的项目再进一步探讨这个问题。首先,在编写Makefile之前,需要先了解对目录结构的需求,包括:

  • 将所有的目录文件放入objs子目录中。
  • 将最终生成的可执行程序放入exes子目录中。

目录的自动创建与删除

在编译目录职期间按,需要将存放生成文件的目录准备好。目录可以在项目编译之前通过手工去创建,但是我们更喜欢在编译过程中自动生成的方式。
要实现在编译过程中自动创建目录,需要记住一点:目录也是一个目标。具有自动创建目录的Makefile和其运行结果如图所示:

.PHONY: all

MKDIR = mkdir
DIRS = objs exes

all: $(DIRS)

$(DIRS):
	$(MKDIR) $@

在这里插入图片描述接下来增加一个clean目标用于清除编译目标目录,如下:

.PHONY: all

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

DIRS = objs exes

all: $(DIRS)

$(DIRS):
	$(MKDIR) $@

clean:
	$(RM) $(RMFLAGS) $(DIRS)

在这里插入图片描述

通过目录管理文件

为了将项目编译时所创建的文件分别放入objs和exes目录中,需要用到Makefile中的一个函数——addprefix。如下修改:

.PHONY: all

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc

DIR_OBJS = objs
DIR_EXES = exes
DIRS = $(DIR_OBJS) $(DIR_EXES)
EXE = complicated
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) $(EXE)

在这里插入图片描述
上面的修改主要有三种变化:

  • 通过运用addprefix函数,为每一个生成的目标文件加上“objs/”前缀,以便生成的文件放入objs目录中。
  • 在构建目标文件的规则中为目标名字机上“objs/”前缀,即增加“$(*DIR_OBJS)/”前缀。
  • 在clean规则的命令中增加对$(EXE)目标的删除。

提升依赖关系管理

现在假设对项目已经进行了一次成功的编译,这一点很重要,否则看不到现有的Makefile存在的问题。接着,将foo.h文件内容进行修改,但是不修改foo.c文件。理论上,头文件和源文件函数声明定义不同应该会编译出错。
在这里插入图片描述
当修改后执行make后,结果居然是告诉我们没有什么事可做,此时如果执行make clean后重新make则发现会报错:
在这里插入图片描述
那么是为什么make不能发现foo.h已经更改而进行重新编译呢。从Makefile文件中的内容能看出来它并不知道有foo.h文件的存在,而之所以编译会用到foo.h是因为foo.c和main.c中包含了头文件。如下:
在这里插入图片描述
为了修复这一点,最简单直接的办法就是将foo.h文件通过依赖关系树纳入make的视野中,改动后的Makefile如下所示:

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc

DIR_OBJS = objs
DIR_EXES = exes
DIRS = $(DIR_OBJS) $(DIR_EXES)
EXE = complicated
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) $(EXE)

执行结果如下:
在这里插入图片描述执行后正常,但是如果将每一个头文件都写入到Makefile的相应规则中,那必然很不妥当。

自动生成文件依赖关系

我们知道通过gcc能够获得一个源文件对其他依赖文件的列表,gcc的这个功能其实就是为make所准备的。使用gcc的-MM选项并且结合sed命令后输出结果如下:(使用sed命令进行替换的目录是为了在目标名前加上“objs/”前缀)

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

在这里插入图片描述gcc还有另一个非常有用的-E选项,这个选项能够告诉gcc制作预处理而不就行程序编译。在生成依赖关系时,其实不需要gcc编译源文件,只有进行预处理获得依赖文件列表就行了。同使用-E选项,可以避免生成依赖关系时gcc发出编译警告以及提高依赖关系生成的效率。
接下来,我们就开始修改Makefile为每一个源文件通过采用gcc和sed生成一个依赖文件,这些文件采用“.dep”后缀结尾,在此,创建一个新的deps目录用于存放生成的依赖关系文件。如下:

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc

DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = complicated
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 "Creating $@ ..."
	@set -e; \
	$(RM) $(RMFLAGS) $@.tmp; \
	$(CC) -E -MM $^ > $@.tmp; \
	sed 's,\(.*\)\.o[ :]*, objs/\1.o: ,g' < $@.tmp > $@ ;\
	$(RM) $(RMFLAGS) $@.tmp

clean:
	$(RM) $(RMFLAGS) $(DIRS) $(EXE)

在这里插入图片描述这修改包含:

  • 增加了DIR_DEPS变量用于保存需要创建的deps目录名,以及将这个变量的值加入到DIR变量中。
  • 删除了目标文件创建规则中对于foo.h文件的依赖,并将这个规则中的自动变量从 < <变回到 ^
  • 增加了DEPS变量用于存放依赖文件。
  • 为all目标增加了对$(DEPS)的依赖。
  • 增加了一个用于创建依赖关系文件的规则。在这个规则的命令中,使用了gcc的-E和-MM选项来获取依赖关系。

注意

  • 对于规则中的每一条命令,make都是在一个新的Shell上运行它的。
  • 如果希望多个命令在同一个Shell中运行,可以使用“;”将这些命令连起来。
  • 当命令很长时,可以用“\”将一个命令分成多行书写。

使用依赖关系文件

Makefile中存在一个include指令,它的作用同C语言中的#include宏指令相同,在Makefile中,可以通过使用include指令将自动生成的依赖关系文件包含进来,从而使得依赖关系文件中的内容成为Makefile的一部分。如下:

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc

DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = complicated
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif

all: $(EXE)

include $(DEPS)
$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@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) $(EXE)

在这里插入图片描述
改动主要增加了三个变量,这三个变量的值根据相应的目录是否存在而分别赋值。对于每个变量,如果对应的目录不存在,则将目录名赋值给它,否则其值为空。增加的三个变量,分别被放到相应的规则中作为第一个先决条件。

为依赖关系文件建立依赖关系

现在,让我们再对complicated项目的源程序文件进行一定的修改,以便于增加程序文件之间依赖关系复杂度。
如下:

  • define.h
#ifndef __DEFINE_H
#define __DEFINE_H

#define HELLO "hello"

#endif
  • foo.h
#ifndef __FOO_H
#define __FOO_H

#include "define.h"

void foo();

#endif
  • foo.c
#include <stdio.h>
#include "foo.h"

void foo()
{
	printf("%s this is foo!\n", HELLO);
}
  • main.c
#include "foo.h"

int main()
{
	foo();
	return 0;
}

其中改动包括:

  • 增加define.h文件并且在其中定义了一个HELLO宏。
  • 在foo.h中包含define.h文件
  • 在foo.c中增加对HELLO宏的引用。
    增加了这些改动后,对项目进行make,结果如下:
    在这里插入图片描述
    这次成功编译项目基础上,(不执行make clean)增加一个other.h文件并将之前在define.h中定义的宏放在这个文件中,另外,让define.h包含other.h文件,如下:
  • other.h
#ifndef __OTHER_H
#define __OTHER_H

#define HELLO "hello"

#endif
  • define.h
#ifndef __DEFINE_H
#define __DEFINE_h

#include "other.h"

#endif

在这里插入图片描述从结果看,已经对foo.c和main.c重新编译,并且执行结果也是最新的。
现在对other.h文件再进行修改。将宏改变,运行make发现:

在这里插入图片描述从结果看发现项目并没有因为更改了other.h文件而重新编译。这是因为other.h的被包含文件是define.h、从.c文件中获取到的依赖关系中并没有关于other.h的依赖关系。
完善方法
为foo.dep和main.dep也引入依赖关系,有了这种依赖关系,make就能发现当对define.h进行修改后需要对依赖关系重新构建,这也就造成了other.h也能够出现在依赖关系树中。

在这里插入图片描述修改Makefile如下:

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc

DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = complicated
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif

all: $(EXE)

include $(DEPS)
$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@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) $(EXE)

在Makefile中只需要在构建依赖关系的规则中增加自动变量$@就行了,因为它表示的是依赖关系文件名。有了这样改动后,就需要执行make clean后重新make构建了。
在这里插入图片描述在这里发现一个问题:当执行两次make clean时显示结果不同,第二次执行make clean会重新构建依赖关系然后清除。
在这里插入图片描述为了去除这一现象,可以运用条件语法。更改后如下:

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc

DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = complicated
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif

all: $(EXE)

ifneq ($(MAKECMDGOALS), clean)
include $(DEPS)
endif

$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@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) $(EXE)

在这里插入图片描述

打造更专业的编译环境

在上一小节中已经说明了,一个好的目录结构对于软件项目的维护至关重要,而Makefile的设计也应当迎合项目目录结构规划的需要。前面的几个示例项目使用的目录结构都很单一,但是大型项目旺旺是采用多个目录来存放不同的模块。接下来通过模拟的huge项目来实现一个更加专业的编译环境。

规划项目目录结构

如下图说明了huge项目采用的目录结构。从中可以看出,huge项目最上层有两个目录,其中一个是build目录,另一个是code目录。前者用于存放各Makefile文件间的共享文件make.rule以及编译整个项目的Makefile。在build目录中还会在编译期间自动生成libs和exes两个子目录。libs目录用于存放编译出来的目标文件,而exes目录用于存放编译出来的可执行文件。
在这里插入图片描述code目录用于存放项目的源程序文件,在code目录下按照各个软件模块分成不同的子目录。huge项目中包括foo库和huge主程序,使用在code目录下分别创建了foo和huge两个子目录。
对于每个软件模块子目录,又分别为用于存放.c文件的src子目录和用于存放.h文件的Inc子目录。当进行项目编译时,我们希望make在src目录下创建deps和objs目录,作用同前面描述的这两个目录的作用一致。
在每一个src目录下都会有一个Makefile,用于构建所在目录中的源程序文件。由此可知,最上层的build目录下的Makefile将会调用每一个软件模块中的src下的子Makefile完成整个项目的构建。
首先需要按照这样的目录结构创建目录:

在这里插入图片描述接下来,我们先创建code/foo/.src下的Makefile,这个可以将前一小节的Makefile进行一定修改得到。如下:

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc
AR = ar
ARFLAGS = crs

DIR_OBJS = objs
DIR_EXES = ../../../build/exes
DIR_DEPS = deps
DIR_LIBS = ../../../build/libs
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS) $(DIR_LIBS)
RMS = $(DIR_OBJS) $(DIR_DEPS)

EXE = 
ifneq ("$(EXE)", "")
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
RMS += $(EXE)
endif

LIB = libfoo.a
ifneq ("$(LIB)", "")
EXE := $(addprefix $(DIR_LIBS)/, $(LIB))
RMS += $(LIB)
endif

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif
ifeq ("$(wildcard $(DIR_LIBS))", "")
DEP_DIR_LIBS := $(DIR_LIBS)
endif

all: $(EXE) $(LIB)

ifneq ($(MAKECMDGOALS), clean)
include $(DEPS)
endif

$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)
$(LIB): $(DEP_DIR_LIBS) $(OBJS)
	$(AR) $(ARFLAGS) $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@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) $(RMS) 

以上修改:

  • 增加了AR和ARFLAGS两个变量,它们被用于静态库的创建。ar工具的使用请参考bintuil编译工具链中的说明。
  • 将exes目录的实际位置以相对路径的形式赋值给DIR_EXES变量。
  • 增加了DIR_LIBS变量以记录libs目录的实际位置,同样采用相对路径的形式。
  • 在DIRS变量中增加了DIR_LIBS变量的值,以便于创建build/libs目录。
  • 新增了RMS变量用于表示需要删除的目录和文件。由于这个Makefile只是针对libfoo.a库的,使用当运行“make clean”时,不应将位于build目录下的exes和libs目录全部删除,这与之前项目实例不太一样。
  • 清除了对EXE变量所赋的值,同时增加了ifneq条件语句用于判断EXE变量的值是否为空。只有当EXE不为空时才需要为EXE变量的值增加目录前缀并将$(EXE)加入到RMS变量中以便调用“make clean”时清除它。
  • 新增了LIB变量,用于存放最终生成库的名字,目前这个值被设置为libfoo.a。同样采用处理EXE变量的方法,使用条件语法来决定是否需要为LIB变量中的值增加目录前缀。
  • 为all目录增加$(LIB)先决条件。
  • 增加了一条用于生成库的规则,在规则的命令体中使用ar工具来生成库。
  • 在clean目标命令中,采用删除RMS变量中的内容而不是DIRS变量中的内容的方式。这一点前面说过了,因为我们不希望在foo模块中运行“make clean”时将build目录下的libs和exes目录也删除。

增进复用性

可以将公用部分放入一个独立的文件中——这就是build目录下make.rule文件的作业。那再foo模块的Makefile中,哪些是不能公用的呢?

  • 变量EXE和LIB的定义对于每一个软件模块是不同的。比如在该项目中,需要将code/foo/src目录下Makefile中的LIB变量设置为“libfoo.a”,且EXE变量应当为空。但是,在code/huge/src目录中的Makefile内却要反过来,只定义EXE变量的值为“huge.exe”。
  • DIR_EXES变量和DIR_LIBS变量由于运用了相对路径,所以也是每个模块特有的。但是可以采用绝对路径的方式解决这个文件。比如,可以定义一个ROOT环境变量,其值设置为huge项目的根目录,这样的话,DIR_EXES和DIR_LIBS就可以以ROOT为相对路径,从而使得其值对于所有的模块都相同。

在考虑复用情况下,foo模块的Makefile由两部分组成,分别是build目录中的make.rule和code/foo/src目录中的Makefile,内容如下:

  • huge/build/make.rule
.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc
AR = ar
ARFLAGS = crs

DIR_OBJS = objs
DIR_EXES = $(ROOT)/build/exes
DIR_DEPS = deps
DIR_LIBS = $(ROOT)/build/libs
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS) $(DIR_LIBS)
RMS = $(DIR_OBJS) $(DIR_DEPS)

#EXE =
ifneq ("$(EXE)", "")
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
RMS += $(EXE)
endif

#LIB = libfoo.a
ifneq ("$(LIB)", "")
LIB := $(addprefix $(DIR_LIBS)/, $(LIB))
RMS += $(LIB)
endif

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif
ifeq ("$(wildcard $(DIR_LIBS))", "")
DEP_DIR_LIBS := $(DIR_LIBS)
endif

all: $(EXE) $(LIB)

ifneq ($(MAKECMDGOALS), clean)
include $(DEPS)
endif

$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)
$(LIB): $(DEP_DIR_LIBS) $(OBJS)
	$(AR) $(ARFLAGS) $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@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) $(RMS)

  • huge/code/foo/src/Makefile
EXE = 
LIB = libfoo.a

include $(ROOT)/build/make.rule

接下来在code/huge/src目录下创建新的Makefile及一个main.c文件来验证执行文件的编译。

  • code/huge/src/Makefile
EXE = huge
LIB = 
include $(ROOT)/build/make.rule
  • main.c
#include <stdio.h>

int main()
{
	printf("hello world!\n");
	return 0;
}
  • 验证:
    在这里插入图片描述

支持头文件目录的指定

现在,是时候将项目文件放入各目录结构中,目前准备huge项目具有三个文件如下:

  • huge/code/foo/inc/foo.h
#ifndef __FOO_H
#define __FOO_H

void foo();

#endif
  • huge/code/foo/src/foo.c
#include <stdio.h>
#include "foo.h"

void foo()
{
	printf("this is foo!\n");
}
  • huge/code/huge/src/main.c
#include "foo.h"

int main()
{
	foo();
	return 0;
}

修改Makefile添加INCLUDE_DIR变量设置

  • huge/build/make.rule
.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc
AR = ar
ARFLAGS = crs

DIR_OBJS = objs
DIR_EXES = $(ROOT)/build/exes
DIR_DEPS = deps
DIR_LIBS = $(ROOT)/build/libs
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS) $(DIR_LIBS)
RMS = $(DIR_OBJS) $(DIR_DEPS)

#EXE =
ifneq ("$(EXE)", "")
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
RMS += $(EXE)
endif

#LIB = libfoo.a
ifneq ("$(LIB)", "")
LIB := $(addprefix $(DIR_LIBS)/, $(LIB))
RMS += $(LIB)
endif

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif
ifeq ("$(wildcard $(DIR_LIBS))", "")
DEP_DIR_LIBS := $(DIR_LIBS)
endif

all: $(EXE) $(LIB)

ifneq ($(MAKECMDGOALS), clean)
include $(DEPS)
endif

ifneq ($(INCLUDE_DIRS), "")
INCLUDE_DIRS := $(strip $(INCLUDE_DIRS))
INCLUDE_DIRS := $(addprefix -I, $(INCLUDE_DIRS))
endif
ifneq ($(LINK_LIBS), "")
LINK_LIBS := $(strip $(LINK_LIBS))
LINK_LIBS := $(addprefix -l, $(LINK_LIBS))
endif

$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -L$(DIR_LIBS) -o $@ $(filter %.o, $^) $(LINK_LIBS)
$(LIB): $(DEP_DIR_LIBS) $(OBJS)
	$(AR) $(ARFLAGS) $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) $(INCLUDE_DIRS) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@set -e; \
	$(RM) $(RMFLAGS) $@.tmp; \
	$(CC) $(INCLUDE_DIRS) -E -MM $(filter %.c, $^) > $@.tmp; \
	sed 's,\(.*\)\.o[ :]*, objs/\1.o $@: ,g' < $@.tmp > $@ ;\
	$(RM) $(RMFLAGS) $@.tmp

clean:
	$(RM) $(RMFLAGS) $(RMS)

  • huge/code/huge/src/Makefile
EXE = huge
LIB = 

INCLUDE_DIRS = $(ROOT)/code/foo/inc
LINK_LIBS = foo

include $(ROOT)/build/make.rule

  • 执行
    在这里插入图片描述

实现库链接

前一步已经完成了库的编译,但是还没有生成可执行文件。添加编译选项实现库链接。

  • huge/build/make.rule
.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc
AR = ar
ARFLAGS = crs

DIR_OBJS = objs
DIR_EXES = $(ROOT)/build/exes
DIR_DEPS = deps
DIR_LIBS = $(ROOT)/build/libs
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS) $(DIR_LIBS)
RMS = $(DIR_OBJS) $(DIR_DEPS)

#EXE =
ifneq ("$(EXE)", "")
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
RMS += $(EXE)
endif

#LIB = libfoo.a
ifneq ("$(LIB)", "")
LIB := $(addprefix $(DIR_LIBS)/, $(LIB))
RMS += $(LIB)
endif

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif
ifeq ("$(wildcard $(DIR_LIBS))", "")
DEP_DIR_LIBS := $(DIR_LIBS)
endif

all: $(EXE) $(LIB)

ifneq ($(MAKECMDGOALS), clean)
include $(DEPS)
endif

ifneq ($(INCLUDE_DIRS), "")
INCLUDE_DIRS := $(strip $(INCLUDE_DIRS))
INCLUDE_DIRS := $(addprefix -I, $(INCLUDE_DIRS))
endif
ifneq ($(LINK_LIBS), "")
LINK_LIBS := $(strip $(LINK_LIBS))
LINK_LIBS := $(addprefix -l, $(LINK_LIBS))
endif

$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS)
	$(CC) -L$(DIR_LIBS) -o $@ $(filter %.o, $^) $(LINK_LIBS)
$(LIB): $(DEP_DIR_LIBS) $(OBJS)
	$(AR) $(ARFLAGS) $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) $(INCLUDE_DIRS) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@set -e; \
	$(RM) $(RMFLAGS) $@.tmp; \
	$(CC) $(INCLUDE_DIRS) -E -MM $(filter %.c, $^) > $@.tmp; \
	sed 's,\(.*\)\.o[ :]*, objs/\1.o $@: ,g' < $@.tmp > $@ ;\
	$(RM) $(RMFLAGS) $@.tmp

clean:
	$(RM) $(RMFLAGS) $(RMS)

  • huge/code/huge/src/Makefile
EXE = huge
LIB = 

INCLUDE_DIRS = $(ROOT)/code/foo/inc
LINK_LIBS = foo

include $(ROOT)/build/make.rule

其中的改动有:

  1. 在make.rule文件中增加了对LINK_LIBS变量的引用,这个变量用来存放可执行程序在链接时所需要用到的所有库。
  2. 在make.rule中将 ( D I R L I B S ) g c c L h u g e (DIR_LIBS)通过gcc的-L选项加入到了编译器的库搜索目录列表中。在huge项目中,采用将所有的库文件都放入 (DIR_LIBS)目录中这种方式简化了Makefile的实现。
  3. 在各模块的src目录下的Makefile中增加了LINK_LIBS变量的定义,并且在code/huge/src/makefile中对LINK_LIBS赋值为“foo”。在linux中,一个静态库的个数是libxxx.a,其中xxx就是采用gcc的-l选项时所需要用到的名。在这里用到的“foo”指的就是libfoo.a库。
  • 执行结果
    在这里插入图片描述

增加一个bar模块

现在,在huge项目中 增加一个bar模块来验证我们编译系统设计的情况。bar这个模块将生成libbar.a静态库,在code目录下新建bar目录以及内部的src和inc,在src中新建并编译好Makefile。具体如下:

  • huge/code/bar/src/bar.c
#include <stdio.h>
#include "bar.h"

void bar()
{
	printf("this is bar()!\n");
}
  • huge/code/bar/inc/bar.h
#ifndef __BAR_H
#define __BAR_H

void bar();

#endif
  • huge/code/bar/src/Makefile
EXE =
LIB = libbar.a

INCLUDE_DIRS = $(ROOT)/code/bar/inc
LINK_LIBS =

include $(ROOT)/build/make.rule

在这里插入图片描述* 执行:
在这里插入图片描述

  • 修改主编译Makefile
EXE = huge
LIB = 

INCLUDE_DIRS = $(ROOT)/code/foo/inc
INCLUDE_DIRS += $(ROOT)/code/bar/inc
LINK_LIBS = foo bar

include $(ROOT)/build/make.rule
  • 执行:
    在这里插入图片描述

增强可使用性

从前面看,为了编译huge项目需要进入不同的目录运行make,这一步可以被简化,需要通过修改build目录下的Makefile来实现。

  • huge/build/Makefile
.PHONY: all clean

ROOT = $(realpath ..)

DIRS = $(ROOT)/code/foo/src \
	   $(ROOT)/code/bar/src \
	   $(ROOT)/code/huge/src

RM = rm
RMFLAGS = -rf
RMS = $(ROOT)/build/exes $(ROOT)/build/libs

all clean:
	@set -e; \
		for dir in $(DIRS);\
		do \
			cd $$dir && $(MAKE) ROOT=$(ROOT) $@; \
		done
	@set -e; \
		if [ "$(MAKECMDGOALS)" = "clean" ]; then $(RM) $(RMFLAGS) $(RMS); fi
	@echo ""
	@echo ":-) Completed"
	@echo ""

  • 执行结果(make编译会执行目录下所有的Makefile文件)
    在这里插入图片描述

管理对库的依赖关系

当我们修改了foo模块中的foo.c文件,重新make后发现libfoo.a库被重新编译了,这是对的,但是发现exes可执行文件目标并没有因此重新被编译。这是因为可执行文件的编译并没有标明对库的依赖,我们修改makefile文件来使得可执行文件依赖库文件。

.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf

CC = gcc
AR = ar
ARFLAGS = crs

DIR_OBJS = objs
DIR_EXES = $(ROOT)/build/exes
DIR_DEPS = deps
DIR_LIBS = $(ROOT)/build/libs
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS) $(DIR_LIBS)
RMS = $(DIR_OBJS) $(DIR_DEPS)

#EXE =
ifneq ("$(EXE)", "")
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
RMS += $(EXE)
endif

#LIB = libfoo.a
ifneq ("$(LIB)", "")
LIB := $(addprefix $(DIR_LIBS)/, $(LIB))
RMS += $(LIB)
endif

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

ifeq ("$(wildcard $(DIR_OBJS))", "")
DEP_DIR_OBJS := $(DIR_OBJS)
endif
ifeq ("$(wildcard $(DIR_EXES))", "")
DEP_DIR_EXES := $(DIR_EXES)
endif
ifeq ("$(wildcard $(DIR_DEPS))", "")
DEP_DIR_DEPS := $(DIR_DEPS)
endif
ifeq ("$(wildcard $(DIR_LIBS))", "")
DEP_DIR_LIBS := $(DIR_LIBS)
endif

all: $(EXE) $(LIB)

ifneq ($(MAKECMDGOALS), clean)
include $(DEPS)
endif

ifneq ($(INCLUDE_DIRS), "")
INCLUDE_DIRS := $(strip $(INCLUDE_DIRS))
INCLUDE_DIRS := $(addprefix -I, $(INCLUDE_DIRS))
endif
ifneq ($(LINK_LIBS), "")
LINK_LIBS := $(strip $(LINK_LIBS))
LIB_ALL := $(notdir $(wildcard $(DIR_LIBS)/*))
LIB_FILTERED := $(addprefix %, $(addprefix lib, $(LINK_LIBS)))
$(eval DEP_LIBS = $(filter $(LIB_FILTERED), $(LIB_ALL)))
DEP_LIBS := $(addprefix $(DIR_LIBS)/, $(DEP_LIBS))
LINK_LIBS := $(addprefix -l, $(LINK_LIBS))
endif

$(DIRS):
	$(MKDIR) $@
$(EXE): $(DEP_DIR_EXES) $(OBJS) $(DEP_LIBS)
	$(CC) -L$(DIR_LIBS) -o $@ $(filter %.o, $^) $(LINK_LIBS)
$(LIB): $(DEP_DIR_LIBS) $(OBJS)
	$(AR) $(ARFLAGS) $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DEP_DIR_OBJS) %.c
	$(CC) $(INCLUDE_DIRS) -o $@ -c $ $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DEP_DIR_DEPS) %.c
	@echo "Creating $@ ..."
	@set -e; \
	$(RM) $(RMFLAGS) $@.tmp; \
	$(CC) $(INCLUDE_DIRS) -E -MM $(filter %.c, $^) > $@.tmp; \
	sed 's,\(.*\)\.o[ :]*, objs/\1.o $@: ,g' < $@.tmp > $@ ;\
	$(RM) $(RMFLAGS) $@.tmp

clean:
	$(RM) $(RMFLAGS) $(RMS)

总结

本文根据李云先生写的书籍《专业嵌入式软件开发—全面走向高质高效编程》中的第三章的内容总结了Makefile的编写方式及构建完整的编译工程。当然,在实际使用过程中,还有很多问题和情况,还需要我们继续去努力发现并改善。

发布了102 篇原创文章 · 获赞 27 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_37596943/article/details/103740759