转载:如何编写Makefile

参考博客原址:https://blog.csdn.net/runninglion/article/details/28111859

一、 三个基本概念

(注:本文所有的测试都是在 Linux 环境下进行的)
  在Makefile中,最重要的三个概念是:目标(target)、依赖关系(dependency)和命令(command)。目标是指要干什么,即运行make后生成什么;依赖是指明目标所依赖的其他目标;命令则告诉make如何生成目标,这三个概念是通过Makefile中的规则(rule)关联在一起的。

   例 1 编辑一个名为 Makefile 的文件,文件内容如下:

all:
    echo "Hello Lion, I love you"

然后在命令行中执行它,键入集合  make ,就能执行。编辑 makefile 文件时要注意,命令所在的行必须以  Tab 键开头。

  在Makefile中,目标和命令组合在一起就形成了一个简单的规则,通过这个规则,我们告诉 make 要做什么。运行 make 命令时可以指定具体的目标加以选择。

例 2  继续编辑修改刚才的 Makefile 文件,如下:

all:
    echo "Hello Lion, I love you"
test:
    echo "Just for test, she is so beautiful"

  综上,我们得到以下信息:一个Makefile中可以定义多个目标;调用 make 命令时,得告诉它我们希望构建的目标是什么,即要它执行哪个命令,第一个目标是默认执行的目标;当 make 得到目标后,先找到构建目标的对应规则,然后运行规则中的命令来达到构建目标的目的,一个规则中可以根据需要存在多条命令。
  如果不想让 make 打印出每条要执行的命令,可以在命令前加上  @ 符号,如

all:
   @ echo "Hello Lion, I love you"
test:
    @echo "Just for test, she is so beautiful"

先决条件:在执行一个目标前,必须要先执行其他目标,即当前目标的执行是以其他目标的执行为条件。这个先决目标就是当前要执行的目标要依赖的目标。如把刚才的 Makefile 修改如下:

all: test
    @echo "Hello Lion, I love you"
test:
    @echo "Just for test, she is so beautiful"

然后再次执行命令 make ,运动结果如下:

$ make
Just for test, she is so beautiful!
Hello Lion, I love you!

  从结果可以看到,test 目标先被构建了,然后才构建 all 目标,因为 test 目标是 all 目标的先决条件。出现这种目标依赖关系时, make 会从左到右(在同一规则中)和从上到下(在不同的规则中)的先后顺序先构建一个规则所依赖的每一个目标,形成一种“链式反应”。


二、 搭建基本的编译环境(实验)

  我们把这个简单的项目称为  simple 项目吧,让我们先编辑项目中用到的几个文件

(1)foo.c

#include <stdio.h>
void foo()
{
    printf("foo() function test makefile");
}

(2) main.c

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

(3) Makefile

all: 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 main.o foo.o

  执行方法很简单,键入 make 命令就会生成相应文件,键入 make clean 命令就会删除相应文件。注意连续几次(大于两次)键入 make 命令,从第二次开始,就没有构建目标文件的动作,但是有构建 simple 可执行程序的动作。这是因为 make 是通过文件的时间戳来判定哪些文件需要重新编译的。make 在分析一个规则以创建目标时,如果发现先决条件中文件的时间戳大于目标的时间戳,那先决条件中的文件比目标更新,就没必要再重新构建了。

三、 让Makefile更专业

1、假目标的运用

  在前面写的 Makefile中,有一个 clean 目标。假设 Makefile 所在的目录下有一个 clean 文件,那么当我们运行 make clean 时,将无法正常执行。因为些时 make 会把 clean 当成文件来处理,而不是当成命令。这种目录文件名与 Makefile 中的目标名重名的情况是很难避免的,也是我们不希望看到的。为此,引进 假目标(phony target这个概念。假目标用  .PHONY 关键字来定义,必须大写。所以我们可以这样修改上边的Makefile文件。

.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 main.o foo.o

  更改后再执行 make clean 命令,它就能正确执行了。使用  .PHONY关键声明一个目标后, make 并不会将其当做一个文件来处理。由于假目标并不与文件关联,所以每次构建假目标时它所在规则中的命令一定会被执行。

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

  为了提高 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 $(OBJS)

  在这个 Makefile 中,我们定义了CC、RM、EXE、OBJS四个变量,定义变量时其值可以为空,即无右值。引用变量时可以采用  $(变量名)  ${变量名} 的形式。引入变量后,Makefile 就很灵活了,例如如果我们想更换编译器,我们只需更改  CC 变量的值就行了。

自动变量

  在上边的这个 Makefile 中,存在目标名和先决条件名在规则中的命令重复出现的情况。如果目标名或先决条件名发生了改变,那么我们就必须相应的修改所有的命令,为了省去这种麻烦,我们可以使用如下自动变量。
$@  用于表示一个规则中的目标,当一个规则中有多个目标时,$@ 所指的是其中任何造成规则命令被运行的目标;
$^  表示的是规则中的所有先决条件;
$<  表示的是规则中的第一个先决条件。
  当然 Makefile 中还有其他的自动变量,但是现在我们只用到这三个。下边是一个测试文件

.PHONY : all

all : first second third
    @echo "\$$@ = $@"
    @echo "$$^ = $^"
    @echo "$$ = $<"

first second third

执行结果如下

$@ = all
$^ = first second third
$< = first

  注:在 Makefile 中 “$” 有特殊的意思,如果想用 echo 输出 “$” ,就必须用两个连着的 “$” ; “$@” 对于 Bash shell 也有特殊的意思,需要在 “$$@” 之前再加一个脱字符 “\” ,最后一行是一个只有目标的规则,是不能缺少的。下面我们再来使用自动变量来重写前面的 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 的命令名是什么。在这篇文章中, $(MAKE) 的值就是 “make” 。当需要在 Makefile 中运行另一个 Makefile 时,需要用到这个变量。 MAKECMDGOALS 变量表示的是当前构建的目标名,下面分别是测试  MAKE 和  MAKECMDGOALS 的 Makefile 文件。注意  MAKECMDGOALS 是指用户输入的目标,所以当只运行 make 而不带参数时(即用户不在命令行中输入任何目标), MAKECMDGOALS 仍为空而不是 “all” 。

测试  MAKE

.PHONY : all

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

测试  MAKECMDGOALS

.PHONY : all clean

all clean :
    @echo "\$$@ = $@"
    @echo "MAKECMDGOALS = $(MAKECMDGOALS)"


变量的类别与赋值

  变量有递归扩展变量和简单扩展变量两种。递归扩展变量只用一个 “  ” 符号进行定义,其赋值是可递归的。
  下面让我们通过一个例子来看一下 递归扩展变量的特点

.PHONY : all

first = $(lion)
lion = $(love)
love = linda

all :
    @echo $(first)

执行 make 命令后,打印结果如下 
linda

  使用递归扩展变量的时候要注意,使用不当可能会造成死循环,如下的赋值就是一个死循环
 CFLAGS = $(CFLAGS) -O

   简单扩展变量,是用 “  := ” 操作符来定义的,对于这种变量,make 只对其进行一次扩展。来看一个例子

.PHONY : all

x = lion
y = $(x) love
x = linda

xx := lion
yy := $(xx) love
xx := linda

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

执行 make 命令后,打印结果如下
x = linda love, xx = lion love

在 Makefile 中可以对同一个变量采用不同的赋值操作,如下例子

.PHONY : all

obj = main.o foo.o
obj := $(obj) utils.o

all :
    @echo $(obj)

执行 make 命令后,打印结果如下
main.o foo.o utils.o

  在 Makefile 中还可以使用 条件赋值,这个操作是通过 “  ?= ” 操作符来实现的,当变量没定义时就定义它,并且将右边的值赋值给它;如果变量已经定义了,则不改变其原值。条件赋值通常用于为变量赋默认值。例

.PHONY : all

x = lion
x ?= linda
y ?= linda

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

执行 make 命令后,打印结果如下
x = lion, y = linda

在 Makefile 中,我们也可以通过 “  += ” 实现追加赋值的功能,例
.PHONY : all

x = main.o
x += object.o

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

执行 make 命令后,打印结果如下
x = main.o object.o

  在 Makefile 中可以对变量进行定义,也可以通过其他方式来让 make 获得变量:对于自动变量,其值是在每一个规则的上下文件中自动获得的;在运行 make 时,可以通过命令参数定义变量,如对于下边的 Makefile

.PHONY : all

x = main.o
x += object.o

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

  如果执行命令 make x=change 则打印结果为
x = change

  由此可见,在运行 make 的命令参数中定义的变量在 Makefile 中是可见的,而且这些参数可以覆盖 Makefile 文件中所定义的变量的值。
  有时我们可能不希望 Makefile 文件中的变量有被覆盖的可能,这时就得用到 override 指令进行预防了。例

.PHONY : all

override x = main.o
x += object.o

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

  执行命令 make x=change 的打印结果为
x = main.o
  可见使用 override 指令后,x 变量的值不可覆盖了,而且 Makefile 中对它进行追加赋值也失效了。

使用 “ 模式 ” 精简规则
  在前边的 simple 项目的 Makefile 文件中,存在多个规则用于构建目标文件。如 foo.o 和 main.o ,都采用不同的规则进行描述。如果对于每个目标文件,都要写一个不同的规则来描述,那是很费力的事。所以我们有必要使用模式来减少我们写 Makefile 的工作量。借助模式,我们可以把 simple 项目的 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 $(OBJS)

   改写后的 Makefile 与原来的 Makefile 的区别就是把

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

  替换成了
%.o : %.c
    $(CC) -o $@ -c $^

  经过这样的修改,把多条构建瞟文件的规则变成了一条,不论有多少个源文件需要编译都可以应用同一规则,编写 Makefile 文件的工作量就大大减少了。(其中的  是通配符)


四、通过函数增强功能

  函数是 Makefile 中经常要用到的,它们可以增强 Makefile 的功能,在 simple 项目中,其 Makefile 文件中要指明每个项目的源
文件,这样是很麻烦的。我们可以通过函数来避免这种麻烦,下面让我们先来看一下一些常用的函数(《GNU make》中有详细的解说)。下面的实验都是我在公司的服务器上做的

1、abspath 函数:用于将 _names 中的各路径名转换成绝对路径,并将转换后的结果返回,其形式是
$(abspath _names)

.PHONY all
ROOT := $(abspath /usr ../lib)
all :
    @echo $(ROOT)

执行 make 的打印结果如下
/usr /home/lion/pratemp/pramake/lib

2、addprefix 函数:用于给名字列表 _names 中的每一个名字增加前缀,并将增加了前缀的名字列表返回,其形式是
$(addprefix _prefix, _names)

.PHONY : all

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

all :
    @echo $(with_dir)

执行 make 的打印结果如下
objs/foo.c objs/bar.c objs/main.o

3、addsuffix 函数:用于给名字列表 _names 中的每一个名字增加后缀,并将增加了后缀的名字列表返回,其形式是
$(addsuffix _suffix, _names)

.PHONY : all

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

all :
    @echo $(with_suffix)

执行 make 的打印结果如下
foo.c bar.c main.c

4、filter 函数:用于将 _text 中根据模式 _pattern 得到满足需要的名字列表并返回,其形式是
$(filter _pattern, _text)

.PHONY : all

obj = foo.c bar.c utils.c vir.s single.h
obj := $(filter %.c %.h, $(obj))

all :
    @echo $(obj)

执行 make 的打印结果如下
foo.c bar.c utils.c single.h

.s 文件被过滤掉了,可见 filter 函数起到过滤的作用

5、eval 函数:eval 函数将使得 make 再一次解析 _text语句,该函数返回空字符串,其形式是
$(eval _text)

.PHONY : all

obj = foo.c bar.c utils.c vir.s single.h
$(eval obj := $(filter %.c %.h, $(obj)))

all :
    @echo $(obj)

执行 make 的打印结果如下
foo.c bar.c utils.c single.h

6、filter-out 函数:用于将 _text 中根据模式 _pattern 滤除一部分名字,并将滤除后的列表返回,其形式是
$(filter-out _pattern, _text)

.PHONY : all

obj = foo1.c foo2.c foo3.c test.c single.h
result = $(filter-out foo%.c, $(obj))

all :
    @echo $(result)

执行 make 的打印结果如下
test.c single.h

7、notdir 函数:用于将 _names 中的各路径名转换成绝对路径,并将转换后的结果返回,其形式是
$(notdir _names)

.PHONY : all

names := $(notdir lion/mode1/src/test.c lion/mode2/src/temp.c)

all :
    @echo $(nams)

执行 make 的打印结果如下
test.c temp.c

8、patsubst 函数:被用于将名字列表 _text 中符合 _pattern 模式的名字替换为 _replacement ,并将替换后的名字列表返回。其形式是
$(patsubst _pattern, _replacement, _text)

.PHONY : all

names = test.c temp.c code.o
result := $(patsubst %.c, %.o, $(names))

all :
    @echo $(result)

执行 make 的打印结果如下
test.o temp.o code.o

9、realpath 函数:用于获取 _names 所对应的真实路径名,并将取得的结果返回,其形式是
$(realpath _names)

.PHONY : all

result := $(realpath ./..)

all :
    @echo $(result)

我当前所在的目录是
/home/lion/pratemp/pramake/pra_2
执行 make 的打印结果如下
/home/lion/pratemp/pramake

10、strip 函数:用于将 _string 中多余的空格去除,并将所得结果返回,其形式是
$(strip _string)

.PHONY : all

first = test.c              main.c
second := $(strip $(first))

all :
    @echo "first = $(first))"
    @echo "second = $(second)"

执行 make 的打印结果如下
first = test.c              main.c
second = test.c main.c

11、wildcard 函数:这是个通配符函数,用于得到当前工作目录中满足 _pattern 模式的文件或目录名列表,其形式是
$(wildcard _pattern)

.PHONY : all

srcFile = $(wildcard *.c)

all :
    @echo $(srcFile)

我当前所在目录下有以下文件
Makefile temp.c test.c
执行 make 的打印结果如下
temp.c test.c


五、提高编译环境的实用性

  前面所讲的 simple 项目是一个简单的项目,下面我们来一步步实现一个更大的项目,且称之为  greater 项目吧。下面是这个项目的初始代码

greater/test.h
#ifndef __TEST_H
#define __ TEST_H

void test();

#endif

greater/test.c
#include <stdio.h>
#include "test.h"

void test()
{
    printf("Take it easy, just for test\n");
}

greater/main.c
#include "test.h"

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

greater/Makefile
.PHONY : all

MKDIR = mkdir
DIRS = objs exes

all : $(DIRS)

$(DIRS) :
    $(MKDIR) $@

执行前 make 前后,当前目录文件的变化过程如下
lion@eserver:~/pratemp/pramake/greater$ ls
main.c Makefile test.c test.h
lion@eserver:~/pratemp/pramake/greater$ make
mkdir objs
mkdir exes
lion@eserver:~/pratemp/pramake/greater$ ls
exes main.c Makefile objs test.c test.h

  在编译一个项目时会产生大量的中间文件,如果中间文件与项目的源文件混在一起,就会很乱,不利于维护。所以在编译过程中应该自动生成用于存放不同类型文件的目录,如将所有的目标文件放入 objs 子目录中,将所有的可执行文件放入 exes 子目录等。接下来为我们的 greater 项目的 Makefile 增加一个 clean 目标,用于删除编译时所产生的存放于 objs 子目录中的所有中间文件,修改后的 Makefile 内容如下:

.PHONY : all clean

MKDIR = mkdir
DIRS = objs exes
RM = rm
RMFLAGS = -rf

all : $(DIRS)

$(DIRS) :
    $(MKDIR) $@
clean :
    $(RM) $(RMFLAGS) objs

通过目录管理文件

  为了将项目编译时所创建的文件分类存放(把 .o 文件放入 objs 子目录中,把可执行文件放入 exes 子目录中),我们得借助前面介绍的常用函数。将 greater 项目的 Makefile 修改如下
.PHONY : all clean

MKDIR = mkdir
RM = rm
RMFLAGS = -rf
CC = gcc
OBJS_DIR = objs
EXES_DIR = exes

DIRS = $(OBJS_DIR) $(EXES_DIR)
EXE = greater
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(OBJS_DIR)/, $(OBJS))

all : $(DIRS) $(EXE)

$(DIRS) :
    $(MKDIR) $@
$(EXE) : $(OBJS)
    $(CC) -o $@ $^
$(OBJS_DIR)/%.o : %.c
    $(CC) -o $@ -c $^

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

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


猜你喜欢

转载自blog.csdn.net/qq_32350719/article/details/89330317
今日推荐