makefile 书写

一、 三个基本概念


(注:本文所有的测试都是在 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 中运行,可以用 “;” 将这些命令连接起来;当命令很长时,可以用 “\” 将一个命令分成多行书写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值