[转]工具参考书之Makefile使用

文章原链接:跟我一起写Makefile


Makefile基本格式

make找到的第一项规则会被当做默认规则使用。

一个规则可分成三个部分:

targets : prerequisites
    command
    ...

或是这样:

targets : prerequisites ; command
    command
    ...
  • targets: 是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。
  • command: 是命令行,如果其不与“target:prerequisites”在一行,那么,必须以[Tab]键开头,如果和prerequisites在一行,那么可以用分号做为分隔。
  • prerequisites: 也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。这个在前面已经讲过了。

如果命令太长,你可以使用反斜框'\'作为换行符。规则告诉make两件事,文件的依赖关系和如何成成目标文件。一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。


一个简单的例子

创建一个count_word.c和一个lexer.l,创建makefile文件内容为:

count_words: count_words.o lexer.o -lfl
        gcc count_words.o lexer.o -lfl -o count_words
count_words.o: count_words.c
        gcc -c count_words.c
lexer.o: lexer.c
        gcc -c lexer.c
lexer.c: lexer.l
        flex -t lexer.l > lexer.c
clean:
        rm lexer.c lexer.o count_words.o count_words

直接输入make命令就可以生成可执行文件count_words了,如果要删除执行文件和中间的目标文件,那么就执行一下make clean。

注意:

  • 1 当依赖关系定好后,下面一行就是如何生成目标文件的操作系统命令了,一定要以一个Tab键开头。 另外,make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期比targets文件新,或者targets不存在,那么make就会执行这下面一行的系统命令。
  • 2 clean不是一个文件,它是一个动作名,冒号后面什么都没有,make就不会自动去找它的依赖性,也不会执行它后面的系统命令。因此,要执行clean就需要显式的指出make clean。
  • 3 如果报错,可能需要先安装flex。

make如何工作

默认方式,直接输入make,则:

  • 1 make会在当前的目录下找到名为“Makefile”或者“makefile”的文件。
  • 2 如果找到,它会把文件中第一个target作为最终的目标文件(如上面例子中的count_words)。
    • 2.1 首先,make会检查目标count_words的prerequisite文件count_words.o, lexer.o 和 -lfl。
    • 2.2 count_words.o通过编译count_words.c生成
    • 2.3 lexer.o通过编译lexer.c 生成,但是lexer.c 并不存在,因此会继续寻找lexer.c的生成方式,并找到了通过flex程序将lexer.l生成为lexer.c。
    • 2.4 最后,make会检查-lfl,-l是gcc的一个命令选项,表示将系统库链接到程序。而”fl”对应的是libfl.a的库。(GNU make 可以识别这样的命令,当一个prerequisite是以这种-l的形式表示出来的时候,make会自己搜索lib.so的库文件,如果没找到则继续搜索lib.a的库文件)。这里make找到的是/usr/lib/libfl.a文件,并将它与程序进行连接。
  • 3 如果count_words文件不存在,或者count_words所依赖的后面的.o文件的修改时间比count_words本身更加新,那么,它会执行后面定义的命令来生成这个count_words文件。如果count_words所依赖的.o文件也不存在,那么make会继续按照前面的方式生成.o文件。
  • 4 找到相应的.c和.h,用来生成.o,然后再用.o完成make的最终任务。

依赖关系

make会一层一层的去找文件的依赖关系,最终编译出第一个目标文件。

重新编译

只要任何prerequisite 比 target新,那么这个目标文件就会被下面的命令重新生成。每一个命令都会被传递到shell中,并在自己的子shell里面执行。

关于错误

如果在寻找过程中出现错误,如文件找不到,则make会直接退出并报错。对于所定义的命令错误或者编译不成功,make是不会理会的,它只负责文件的依赖性。

变量使用

为了让makefile更容易维护,在makefile中我们可以使用变量,或者更确切的说是一个字符串,类似c语言中的宏。例如:

CC = gcc
object = lexer.o count_words.o
count_words: $(object) -lfl
        $(CC) $(object) -lfl -o count_words
count_words.o: count_words.c
        $(CC) -c count_words.c
lexer.o: lexer.c
        $(CC) -c lexer.c
lexer.c: lexer.l
        flex -t lexer.l > lexer.c
clean:
        rm lexer.c $(object) count_words

自动推导依赖关系

make可以根据.o文件的文件名自动推导出同名的.c文件并加入依赖关系。并且gcc -c也会被自动推导出来,这种方法也叫“隐式规则”,于是我们的makefile就变成了:

CC = gcc
object = lexer.o count_words.o
count_words: $(object) -lfl
        $(CC) $(object) -lfl -o count_words
count_words.o:
lexer.o:
lexer.c: lexer.l
        flex -t lexer.l > lexer.c
clean:
        rm lexer.c $(object) count_words

关于Clean

一个好习惯是每个makefile都要写clean规则,这样不仅可以方便重编译,也有利于保持文件路径的清洁。一般的风格是:

clean:
        rm lexer.c $(object) count_words

书写规则

规则包含两个部分,一个是依赖关系,一个是生成目标的方法。在 Makefile 中,规则的顺序是很重要的,因为,Makefile 中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让 make 知道你的最终目标是什么。一般来说,定义在 Makefile 中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。 make所完成的也就是这个目标。

通配符

make支持的通配符与Bourne shell基本相同,包括~, *, ?, [...], [^...]

  • *.*:表示了所有文件;
  • ?:表示任意单个字符;
  • [...]:表示一个字符类;
  • [^...]:表示相反的字符类。
  • ~:表示当前用户的/home路径,”~+用户名”可以表示该用户的/home路径。

注意: make会在读取makefile的时候就自动补充好通配符替代的内容,而shell则是在执行命令的时候才会进行通配符替代,在某些复杂情况,这两种方式会有极大的区别。

伪目标

make提供了一种特殊目标: “.PHONY”,用来表示目标文件不是真正的文件,即伪目标。clean命令可以被写作:

.PHONY: clean
clean:
        rm -f *.o lexer.c

即使名为clean的文件存在,make也会执行clean后面的命令,不会造成误解。rm命令前面的减号则表示,不管出现什么问题都要继续做后面的事情。clean规则不要放在makefile的开头,不然就会变成make的默认目标了。

伪目标也可也作为makefile的默认目标,放在文件的最前端,由于伪目标的特性,他指出的所有prerequisite都会被重新编译。这样可以用来同时生成多个目标。另外,与普通目标文件一样,伪目标也可也使用依赖关系,例如:

.PHONY: clean cleano cleanc
clean: cleano cleanc
        -rm $(program)
cleano:
        -rm *.o
cleanc:
        -rm lexer.c

这样就可以对不同类型的文件进行单独删除。

多目标

Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。

bigoutput littleoutput: text.g
    generate text.g -$(subst output,,$@) > $@

上述规则等价于:

bigoutput: text.g
    generate text.g -big > bigoutput
littleoutput: text.g
    generate text.g -little > littleoutput

其中,-$(subst output,,$@)这个函数是截取字符串的意思,“$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。

静态模式

静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活 。我们还是先来看一下语法:

<targets ...>: <target-pattern>: <prereq-patterns ...>
    <commands>
    ....
  • targets 定义了一系列的目标文件,可以有通配符,是目标的一个集合。
  • target-parrtern 是指明了targets的模式,也就是的目标集模式。
  • prereq-parrterns 是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。

看一个例子:

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@

上面的例子中,指明了我们的目标从$object 中获取;目标集模式“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”;依赖模式“%.c”则取模式“%.o”“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,依赖目标就是“foo.c bar.c”。

于是,上面的规则展开后等价于下面的规则:

foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o

查找文件

通常情况下,头文件会放到include下,源文件被放到src,将Makefile放在根目录下,为了让make能够找到相应的位置,需要在makefile开头添加VPATH参数,显式的指出源文件和头文件的路径,不仅make需要知道路径,gcc同样需要:

VPATH = src include
CPPFLAGS = -I include

注意:

  • 1 VPATH变量可以包含一个路径列表,当make需要一个文件时会在其中搜索。这个列表既可以作为目标文件,也可作为关联文件的路径,但不能作为下面命令行程序中文件的路径。这正是为什么在命令行程序中使用自动化变量的原因,避免因为路径修改而导致的命令运行错误。

  • 2 如果是因为make的相关路径配置错误,会使make停止运行;但如果是因为gcc的头文件路径配置错误,会使gcc编译停止。

  • 3 在UNIX系统中,路径列表可以被空格或者冒号分隔开,在Windows中则是用空格或者分号。

  • 4 make会在每次需要文件的时候搜索VPATH列表中的路径,如果有两个不同路径下文件重名,则make只会使用顺序查找到的第一个。

更加准确的方式是使用vpath变量,它的语法是:

vpath pattern directory-list

因此,上面makefile中的VPATH可以写做:

vpath %.c src
vpath %.l src
vpath %.h include

这样就告诉了make去src中寻找.c和.l文件,去include中寻找.h文件。


变量

变量最简单的形式就是 $(variable_name)。变量可以包含几乎所有的字符包括标点符号。一般情况下,变量名需要被$()所包裹,但是当变量名只有一个字符时,括号可以省略。如果要使用真实的“$”字符,用“$$”来表示。
变量赋值可以使用“=”或者“:=”,主要区别是,当用一个变量给别一个变量赋值值时,“:=”只能使用已经定义好的变量,而“=”可使用后面定义的变量。

obj = lexer.o
txt := $(obj)/txt
obj += count_words.o

自动变量

Makefile可以定义很多变量,但同时make本身也定义了一些自动变量。自动变量是make自动根据规则生成的,不需要用户显式的指出相应的文件或目标名称。以下是几个核心的自动变量:

  • $@:目标文件的文件名;
  • $%:仅当目标文件为归档成员文件(.lib 或者 .a)时,显示文件名,否则为空;
  • $<:依赖(prerequisite)列表里面的第一个文件名;
  • $?:所有在prerequisite列表里面比当前目标新的文件名,用空格隔开;
  • $^:所有在prerequisite列表中的文件,用空格隔开; 如果有重复的文件名(包含扩展名),会自动去除重复;
  • $+:与$^相似,也是prerequisite列表中的文件名用空格隔开,不同的是这里包含了所有重复的文件名;
  • $*:显示目标文件的主干文件名,不包含后缀部分。

此外,上面的每个变量都带有两个不同的变种,用于适应不同种类的make。分别是在后面附加一个“D”或者“F”。例如,$(^D)就是代表所有依赖文件的路径,$(<F)表示依赖文件第一个的文件部分的值。使用上述内容前面的makefile可以重写为:

CC = gcc
object = lexer.o count_words.o
program = count_words
$(program): size $(object) -lfl
        $(CC) $(object) -lfl -o $@
count_words.o: count_words.c
        $(CC) -c $^
lexer.o: lexer.c
        $(CC) -c $^
lexer.c: lexer.l
        flex -t $^ > $@
size: count_words.o
        size $^
        touch size
.PHONY: clean cleano cleanc
clean: cleano cleanc
        -rm $(program)
cleano:
        -rm *.o
cleanc:
        -rm lexer.c

目标变量

只在一个Target时才起作用,相当于局部变量。

如下示例:

  • make: 输出Test
  • make test: 依次输出Prog和Test
  • make prog: 输出Prog
prog: A = prog
A = test
default:
    @echo $(A)
test: prog
    @echo $(A)
prog:
    @echo $(A)

高级用法

这里介绍两种变量的高级使用方法。

  • 1 变量值的替换

我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。一个示例:

foo:=a.o b.o c.o
bar:=$(foo:.o=.c)

这个示例中,我们先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c c.c”。

还有基于“静态模式”的变量替换:

foo:=a.o b.o c.o
bar:=$(foo:%.o=%.c)

这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”字符,这个例子同样让$(bar)变量的值为“a.c b.c c.c”。

  • 2 把变量的值再当成变量
x = y
y = z
a := $($(x))

在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,是“x=y”,而不是“x=$(y)”

定义命令包

如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始,以“endef”结束,如:

define run_yacc
    yacc $(firstword$^)
    mv y.tab.c $@
endef

这里,“run_yacc”是这个命令包的名字,不要和Makefile中的变量重名。

define本质上是定义一个多行变量,可以在call的作用下当作函数来使用,在其它位置使用只能作为多行变量的使用。其实单行变量也可以在call下当作函数使用,并且使用$(1)、$(2)实现函数参数传递。

list_file = $(filter $(1),$(2))
bootfiles = $(call list_file,boot,*.c))

条件判断

条件表达式的语法为:

<conditional-directive>
    <text-if-true>
endif

以及:

<conditional-directive>
    <text-if-true>
else
    <text-if-false>
endif

其中<conditional-directive>表示条件关键字,有四个:

  • ifeq (,): 比较参数“arg1”和“arg2”的值是否相同。
  • ifneq (,): 比较参数“arg1”和“arg2”的值是否相同,如果不同,则为真,和“ifeq”类似。
  • ifdef : 如果变量variable-name的值非空,那到表达式为真。否则,表达式为假。当然,同样可以是一个函数的返回值。注意,ifdef只是测试一个变量是否有值,其并不会把变量扩展到当前位置。
  • ifndef : 和“ifdef”相反。

函数

在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。函数调用后,函数的返回值可以当做变量来使用。函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:

$(<function> <arguments>)

或是:

${<function> <arguments>}

这里,function就是函数名,arguments是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。

字符串处理函数

  • subst: 字符串替换
  • patsubst: 模式字答串替换
  • strip: 去除空格
  • findstring: 查找字符串函数
  • filter: 过滤函数
  • filter-out: 反过滤函数
  • sort: 排序函数
  • word: 取单词函数
  • wordlist: 取单词串函数
  • words: 单词个数统计函数
  • firstword: 首单词函数

文件名操作函数

  • dir: 取目录函数
  • notdir: 取文件函数
  • suffix: 取后缀函数
  • basename: 取前缀函数
  • addsuffix: 加后缀函数
  • addprefix: 加前缀函数
  • join 连接函数

流程控制函数

  • foreach函数

循环函数,它的语法是:

$(foreach<var>,<list>,<text>)

这个函数的意思是,把参数list中的单词逐一取出放到参数var所指定的变量中,然后再执行text所包含的表达式。每一次text会返回一个字符串,循环过程中,text的所返回的每个字符串会以空格分隔,最后当整个循环结束时,text所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。所以,var最好是一个变量名,list可以是一个表达式,而text中一般会使用var。

  • if函数

if函数条件语句ifeq,if函数的语法是:

$(if<condition>,<then-part>)

或是:

$(if<condition>,<then-part>,<else-part>)

可见,if函数可以包含“else”部分,或是不含。condition参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,then-part会被计算,否则else-part会被计算。而if函数的返回值是,如果condition为真(非空字符串),那个then-part会是整个函数的返回值,如果condition为假(空字符串),那么else-part会是整个函数的返回值,此时如果else-part没有被定义,那么,整个函数返回空字串。所以,then-part和else-part只会有一个被计算。

  • call函数

call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:

$(call <expression>,<parm1>,<parm2>,<parm3>...)

当make执行这个函数时,expression参数中的变量,如$(1)$(2)$(3)等,会被参数parm1,parm2,parm3依次取代。而expression的返回值就是call函数的返回值。例如:

reverse = $(1) $(2)
foo = $(call reverse,a,b)

那么,foo的值就是“a b”。当然,参数的次序是可以自定义的,不一定是顺序的。

  • origin函数

origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的。其语法是:

$(origin <variable>)

注意,variable是变量的名字,不应该是引用。所以你最好不要在variable中使用“$”字符。origin函数会以其返回值来告诉你这个变量的“出生情况”,下面是origin函数的返回值:

返回值 定义
“undefined” variable从来没有定义过
“default” variable是一个默认的定义,比如“CC”这个变量
“environment” variable是一个环境变量,并且当Makefile被执行时,“-e”参数没有被打开
“file” variable这个变量被定义在Makefile中
“command line” variable这个变量是被命令行定义的
“override” variable是被override指示符重新定义的
“automatic” variable是一个命令运行中的自动化变量
  • shell函数

shell函数参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:

contents := $(shell cat foo)
files := $(shell echo *.c)
  • error函数:产生一个错误信息,同时make退出
  • warning函数:输出一段警告信息,但不会让make退出,而是继续执行

附:%标记和系统通配符*的区别

文章原链接:Makefile中的%标记和系统通配符*的区别

Makefile中的%标记和系统通配符*的区别在于,*是应用在系统中的,%是应用在这个Makefile文件中的。
(本文的测试环境是Windows7下使用MinGW提供的make.exe)

例如,如果你想编译一个文件夹下的所有.c文件,你可能会这样写:

%.o:%.c
    gcc -o $@ $<

但是如果整个文件只有这两行的话,就会出现这样的错误:

Make: *** target not found. stop.

要知道原因,我们先来看看另一个makefile的运行过程,例如有Makefile如下:

test1.o:test1.c
    gcc -o test1.o test1.c
 
test2.o:test2.c
    gcc -o test2.o test2.c

all:test1.o test2.o

如果没有指定输出项目的时候Make会自动找到makefile中第一个目标中没有通配符的目标进行构造,所以步骤是:

  • 1.构造all,发现需要test1.o和test2.o。
  • 2.这个时候他就会在Makefile文件中找到目标能匹配test1.o和test2.o的规则。
  • 3.找到test1.o的规则并且知道test1.c存在,运行下面的命令。
  • 4.同步骤三构造出test2.o。
  • 5.现在构造all的源文件已经齐全,构建all。

其中最重要的是第2步。

Makefile的通配符是在带着目的(如“寻找test1.o”)的时候才会把他要寻找的目标套用通配符%中。

所以通配符%的意思是:

我要找test1.o的构造规则,看看Makefile中那个规则符合。
然后找到了%.o:%.c,来套一下来套一下: %.o 和我要找的 test1.o 匹配,套上了,得到%=test1。
所以在后面的%.c就表示test1.c了。OK进行构造

而通配符*的意思是:

我不知道目标的名字,系统该目录下中所有后缀为.c的文件都是我要找的。
然后遍历目录的文件,看是否匹配。找出所有匹配的项目。

所以虽然连个符号的意思有点沾边,但是他们的工作方式时完全不一样。现在知道了为什么文件中只有

%.o:%.c
    gcc -o $@ $<

会找不到目标了吧。因为没有-f参数时Make会自动找到makefile中第一个目标中没有通配符的目标进行构造,所以就等于找不到目标了。它的意思并不会自动把文件中所有的文件都编译。所以正确的代码应该是:

all:$(subst .c,.o,$(wildcard *.c))
%.o:%.c
    gcc -o $@ $<

这才是把目录下所有文件都编译的命令。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [ yehuohan@gmail.com ]

文章标题:[转]工具参考书之Makefile使用

本文作者:Y

发布时间:2018-11-09, 11:45:01

最后更新:2020-08-20, 16:06:44

原始链接:http://yehuohan.github.io/2018/11/09/%E7%AC%94%E8%AE%B0/DOC/%E5%B7%A5%E5%85%B7%E5%8F%82%E8%80%83%E4%B9%A6%E4%B9%8BMakefile%E4%BD%BF%E7%94%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。