GNU make(2)

GNU make(2)

参考:
GNU Make学习总结(二)java

变量

变量由一个前导符号$加上字符或者是括号字符组成, 名称区分大小写.
命名: 习惯上用所有大写字符表示常量, 小写字符表示变量, 单词之间用下划线.shell

变量赋值

有四种方式:数据库

CC = gcc
CC := gcc
CC ?= gcc
CC += gcc

CC = gcc 这种方式和普通语言不同, 并非直接将右值赋给左值, 而是在变量CC在使用时赋值, 因此将变值赋给变量时, 有可能不一样时候获得不一样的值.vim

CC := gcc 和普通赋值同样, 直接将右值赋给左边.bash

CC ?= gcc 条件赋值, 只有在变量不存在的时候才会赋值, 这种方式很容易和环境变量交互.ide

CC += gcc 同其余语言, 将右值增长到左边的变量后面.函数

除了单个变量, Makefile中还能够用define来定义一个宏来保存一组操做, 用来重用. 例子:学习

define newdir
    mkdir tmp
    cd tmp
endef

拓展变量

make对于变量的拓展不是即时的, 而是有内部的数据库和依存图(Depency Parsing Gragh)的, 须要知道什么时候才会对变量进行拓展.测试

拓展规则:
make会分为两个阶段来完成工做,第一阶段读进makefile以及include进来的makefile,其中定义的变量和规则都会被加进make的内部数据库,并创建依存图,第二阶段会根据依存图判断须要进行哪些更新操做。
下面是用来处理什么时候扩展变量的几条准则:spa

  1. 在变量左侧的部分,会在第一阶段当即扩展。(变量左侧也能够是一个\(x形式的变量,例如x=y,\)(x)=z就等于y=z)
  2. =和?=都在使用的时候,也就是第二阶段才扩展。
  3. :=的右边的值会在第一阶段当即扩展。
  4. +=左边若是是简单变量,右边就会当即扩展,不然会延到第二阶段扩展。(所谓简单变量就是指用:=赋值的变量,相对的,递归变量指用=赋值的变量)
  5. 宏定义的变量名会被当即扩展,宏的主体会被延后到使用时扩展。
  6. 对于每条规则,工做目标和必要条件都是当即扩展,命令老是延后扩展。

能够总结成下表:

定义 扩展a 扩展b
a=b 当即 延后
a?=b 当即 延后
a:=b 当即 当即
a+=b 当即 当即或延后
define a
b..
endef
当即 延后

专属变量

有时候一个变量对于多数规则都适用,但在某条规则下须要对这个变量进行扩展或者改变,这时就能够用到条件专属变量。好比说咱们在编译a.o时要DEFINE DEBUG,能够这样写:

a.o: CFLAGS += -DDEBUG
a.o: a.h

这样的话在编译a.o时CFLAGS会在原来的CFLAGS后面加上-DDEBUG,编译完a.o后就还原了。
扩展到不一样的赋值方式,共有如下四种格式,注意这些赋值操做都会延后到开始处理工做目标的时候进行,变量的值也只在处理该工做目标的时候有效。

注意:
在一个target内部的改变的规则只在编译该target时才有效, 不是全局的.
该规则和给变量的赋值同.以下:

target...: v = value
target...: v := value
target...: v += value
target...: v ?= value

include指令

在上一章生成自动依赖的时候用到了include指令,这里对include指令详细介绍。
在make读到include指令的时候,若是include文件存在,则会读取文件内容并继续执行下去,若不存在,会在汇报问题后继续读取剩下的makefile。读取完成后,make会从规则库中找出任何可用来更新引入文件的规则,若是找到了就执行更新操做,若是一个引入文件被规则更新,则make会清除内部数据库而且从新读进整个makefile,若是这以后include的文件仍然不存在,则报错终止执行。
使用-include或者sinclude指令来代替include可让make忽略没法加载的引入文件。

条件指令

选择性执行:

if-condition
    ...
endif

if-condition
    ...
else
    ...
endif

其中if-condition能够是如下之一

ifdef variable-name
ifndef variable-name
ifeq test
ifneq test

注意的是variable-name不须要加前导$符,而test能够表示成”a” “b”或 (a,b)。

标准make变量

除了自动变量以外,make会为本身的状态和内置规则的定义提供变量。
为内置规则提供的变量经过make -p能够看到,咱们能够经过直接修改这些参数来改变内置规则参数,好比CFLAGS修改编译C时的参数,CC修改C编译器等等。
而为表示Makefile本身状态提供的变量主要有如下几个。

MAKE_VERSION    GNU Make版本号
CURDIR          正在执行make进程的工做目录
MAKEFILE_LIST   make所读进的各个makefile文件名称构成的列表,最后一个是自身文件名
MAKECMDGOALS    make命令指定了哪些工做目标
.VARIABLES      make从各个makefile文件读进的名称所构成的列表

例子:
好比,咱们在clean的时候是不执行include指令的,能够经过如下几行来完成

ifneq "$(MAKECMDGOALS)" "clean"
 -include ...
endif

函数

内置函数

如下只作简单的介绍,具体怎么用试一下就记住了。

内置函数在语法上基本都是如同$(func-name arg1[, argn..])的形式,不一样参数之间用逗号隔开,须要注意的是,除了第一个参数,逗号后的空格都会被保留下来,若是不当心多个空格会引发各类问题。另外,对于处理单词或文件名的函数,参数常常是一串空格隔开的单词,函数对每一个单词进行匹配或处理.

杂项函数

$(sort list)
对list参数排序并去重,此外,它还会删除前导及结尾的空格。虽然这个函数叫sort,但更多的时候是用它来去重。

$(shell command)
将command传递给subshell执行,并将标准输出值做为结果返回,其中换行符都会被替换为空格。

$(strip text)
去掉前导和后面空格,并将内部连续空格转化为单一空格。

$(origin variable)
返回变量的来源,也能够测试变量是否认义,返回值有如下几个:

undefined 未定义
default 来自make内置数据库
environment 来自环境变量
environment override 来自环境变量,并且使用了–environment-overrides指令
file 来自makefile
command line 来自命令行
override 来自override指令
automatic make所定义的自动变量

这里override变量是指在变量赋值前加override,使得该赋值比命令行赋值优先级高,而–environment-overrides选项是使默认环境变量比makefile中环境变量赋值优先级高。

$(warning text)
打印警告信息.

字符串函数
$(filter pattern ...,text)
$(filter-out pattern ...,text)

filter将text视为一系列空格隔开的单词,返回与pattern符合的单词,pattern中可使用模式通配符。而filter-out找的是filter的补集。

$(findstring string...,text)
用处不是太大,在文本中找一个字符串,还不能使用通配符。。

$(subst search-string,replace-string,text)
$(patsubst search-string,replace-string,text)
$(variable:search-string=replace-string)

subst将text中出现search-string的地方所有替换成replace-string,而patsubst与subst的不一样的是可使用一个模式通配符%。第三个函数叫作替换引用,主要是用来替换文件后缀的,与subst不一样的是,search-string必定出如今文件结尾。
如下尝试使用这三个函数替换文件后缀,以理解这几个函数之间的区别:

.PHONY: test
sc = a.c b.c c.c.c
to1 = $(subst .c,.o,$(sc));
to2 = $(patsubst %.c,%.o,$(sc));
to3 = $(sc:.c=.o);
test:
    @echo $(to1)
    @echo $(to2)
    @echo $(to3)

输出结果以下,能够发现subst函数对于文件名中也出现.c的单词会更改文件名,另外两个函数的效果相同,都能替换文件后缀。

a.o b.o c.o.o
a.o b.o c.c.o
a.o b.o c.c.o
$(words text)
$(words n,text)
$(firstword text)
$(wordlist start,end,text)

这几个函数中,text都是用空格隔开的单词列表。第一个函数返回单词的个数,第二个函数返回第n个单词,第三个函数返回第一个单词,第四个返回从start到end的单词。

文件名函数

Makefile中不少时候都是在对文件名进行处理,这些函数常常会用一个变量包含一组文件名,中间用空格隔开,下面的函数中变量后加…都表示这个变量是一个由空格隔开的字符串。

$(wildcard pattern...)
$(dir list...)
$(notdir name...)

第一个函数比较经常使用,可使用通配符来匹配一组文件,这些文件名之间用空格隔开做为函数返回值,好比$(wildcard *.cpp)能够得到当前目录下全部的cpp文件。第二个函数返回list中每一个文件的目录部分,而第三个函数会返回文件名部分。

$(suffix name...)
$(basename name...)
$(addsuffix suffix,name...)
$(addprefix prefix,name...)
$(join prefix-list,suffix-list)

suffix函数返回name列表中的全部后缀,basename返回不带后缀的部分,而addsuffix和addprefix顾名思义,就是为name列表中全部单词添加后缀或前缀。而join就是将prefix-list和suffix-list中的单词按顺序组合,可用来重建被dir和notdir分解的列表。

下面举几个例子来看一下文件名函数的使用方法

#当前目录下以.c和.h文件组成的变量
sources := $(wildcard *.c *.h)
#判断主目录下是否存在.emacs文件
dot-eamcs-exists := $(wildcard ~/.emacs)
#显示包含C文件的子目录(使用find查找,sort去重)
source-dirs := $(sort $(dir $(shell find . -name '*.c')))
#返回$JAVAFILE变量(a.java的形式之间用空格隔开组成)中的JAVA类名
class-name := $(notdir $(subst .java,,$(JAVAFILE)))
#测试$files中是否全部单词具备相同的后缀(判断去重后是否只有一种后缀)
same-suffix = $(filter 1,$(words $(sort $(suffix $files))))
#从Java文件名转换成class名(包含包名,a/b/c.java=>a.b.c)
ftc-name := $(subst /,.,$(basename $(JAVAFILE)))
#计算PATH环境变量中全部程序的数量。最后三行都是处理特殊状况的,处理完后使用空格替代冒号做为分隔符并去重,再在每一个目录后加/*并做为wildcard的参数,最后用words统计wildcard匹配到的程序数目。
program-nums =  $(words \
        $(wildcard  \
            $(addsuffix /*, \
                $(sort  \
                    $(subst :, ,    \
                        $(subst ::,:.:, \
                            $(patsubst :%,.:%,  \
                                $(patsubst %:,%:.,$(PATH)))))))))
流程控制

$(error text)
输出错误信息,并在这以后结束make程序。

$(if condition,then-part,else-part)
与前面提到的条件指令不一样,这里的condition能够是一个函数或者表达式,then-part和else-part也能够是宏或者函数,会根据condition返回的结果是否为空决定执行哪一部分,这里的为空指的是不包含任何字符(包括空格)。
下面这个例子用来判断make是否在3.81版本下执行

$(if $(filter $(MAKE_VERSION),3.81),,$(error version not 3.8.1))

$(foreach variable,list,body)
对于list中的每一个变量variable,执行body部分的内容。每次body部分的内容会被以空格为分隔符累计起来,最后做为返回值。

自定义函数

其实make中定义函数的方式和定义变量的方式几乎同样,有单行定义和宏定义两种方式,单行定义通常用来替代一系列内置命令组合,而对于复杂一点的过程,就要用宏来定义。
make中函数的调用方法以下:
$(call func-name[, param1...])
而在函数体中,则是用$0来表示func-name,\(1~\)n来表示传递的参数,这点和shell的传值是同样的。
下面经过一个实例来展现怎样自定义函数,这个makefile示例如何去写一个简单的调试追踪函数,样例在命令中调用了函数b,而b又调用了a。另一个file-num则是一个单行定义的函数,返回src下指定格式的文件数。

debug_trace = 1
echo-args = $(foreach a,1 2 3,'$($a)'))
debug-enter = $(if $(debug_trace),$(warning Entering $0($(echo-args))))
debug-leave = $(if $(debug_trace),$(warning Leaving $0))
file-num = $(words $(wildcard src/*.$1))
define a
    $(debug-enter)
    @echo $1 $2 $3
    @echo $(call file-num,c)
    $(debug-leave)
endef
define b
    $(debug-enter)
    $(call a,$1,$2,hello)
    $(debug-leave)
endef

.PHONY: test
test:
    $(call b,123,$(CC))

make输出以下,经过输出能够看到这两个函数的运行过程。
`Makefile:21: Entering b('123' 'cc' '')) Makefile:21: Entering a('123' 'cc' 'hello')) Makefile:21: Leaving a Makefile:21: Leaving b 123 cc hello 3

接下来介绍eval函数,eval函数的用途是将文本直接放入make解析器,首先make会扫描eval参数中是否有变量须要进行替换,若是有的话先替换变量,接下来make会再解析文本并进行求值操做。
eval函数的理解有些困难,下面经过一个实例来讲明。

.PHONY: test
test:
    @echo $(obj)

src = tt_a.c tt_b.c tt_c.c
define func
 head = $(patsubst %.c,%.h,$1)
 obj = $(head:.h=.o)
 $(obj):$(head)
endef
$(call func, $(src))

这个makefile的功能比较容易看懂,就是传进去.c文件,分别替换后缀成.h和.o,再创建依赖关系。可是悲剧的是,这个makefile是会报错的,缘由是make不容许在顶层将一个宏扩展成多行(只有在命令的地方能够)。解决这个问题须要用到eval函数,将函数调用那一行改为

$(eval $(call func,$(src)))

此次能够经过编译了,可是仍然悲剧的是,打印出的obj是空的,经过make –print-data-base也看不到咱们定义的规则。这是由于在eval第一遍读取宏的时候,会对变量进行替换,这些替换只依赖于在调用这个宏以前就已经有的变量以及函数传递的变量,而在调用宏以前,head是空的,从而使\((obj)和\)(head)都被替换为空。解决的方法是使用\[来表示变量,这样eval在第一遍读取的时候只会将\]变成$,接下来eval会进行第二遍读取并执行整个函数,再处理以前的变量。最后,通过改动的makefile以下

.PHONY: test
test:
    @echo $(obj)

src = tt_a.c tt_b.c tt_c.c
define func
 head = $(patsubst %.c,%.h,$1)
 obj = $$(head:.h=.o)
 $$(obj):$$(head)
endef

$(eval $(call func,$(src)))

make和make --print-data-base | grep tt_ 结果以下,能够看到obj变量内容正确,也正确生成了依赖

```

make

tt_a.o tt_b.o tt_c.o

make --print-data-base|grep tt_

tt_a.o tt_b.o tt_c.o
head = tt_a.h tt_b.h tt_c.h
src = tt_a.c tt_b.c tt_c.c
tt_a.o: tt_a.h tt_b.h tt_c.h
tt_c.h:
tt_b.o: tt_a.h tt_b.h tt_c.h
tt_a.h:
tt_c.o: tt_a.h tt_b.h tt_c.h
tt_b.h:
...

命令

命令是每条规则的三种组成元素之一,它的实质就是一个单行的shell命令,对于大多数命令,make会将它传给subshell去执行,对于某些不会影响make程序行为的shell命令,make会避免去fork/exec,直接在make中执行。

make默认使用的是/bin/sh,用户也能够经过修改SHELL变量来更改使用的shell。为了编写具备可移植性的makefile,可使用/usr/bin/bash,bash是GNU/Linux采用的标准shell,能够在大多数系统上运行。

在makefile中全部以TAB开头的文本行都被认为是命令,可是像注释以及条件处理命令即便以TAB开头,也会被make识别出来并正确处理。而空行则会被Makefile直接忽略掉。

长命令

须要注意的是,make是以行为单位将命令输送到shell的,若是一个命令超过一行,须要进行处理,下面举一个简单的例子

.PHONY: test
test:
    cd src
    ls

该makefile试图进入src目录并显示src下的文件列表(固然ls src是OK的,这里只是为了举例),但运行后会发现ls显示的还只是make所在目录下的文件,这是由于两行命令被传给了两个不一样的subshell,从而变的不具备关联性。若是想要两条命令在一个subshell中执行,咱们可使用反斜杠符将命令连成一行并在命令间加上分隔符。下面两种写法均可以实现,但其实是有所区别的,在后面的错误处理中会说这个问题。

# way 1
.PHONY: test
test:
    cd src &&   \
    ls
# way 2
.PHONY: test
test:
    cd src;     \
    ls

命令修饰符
@
Makefile默认会输出命令自己,而在命令前加上@能够禁止make的这种行为。加上@的好处是使make输出较容易阅读,坏处是使命令调试变的困难。建议使用一个含@的变量,并用在命令上,经过修改这个变量就能够决定程序的行为

QUIET = @
test:
    @(QUIET) shell script..

-
指示make忽视改行发生的错误,通常当make遇到一个命令返回错误时,会中止make脚本的执行。但若是命令前加上了破折号,make就会忽略错误并继续执行。

+
要求make执行命令,即便是–just-print或-n命令来执行make,在编写递归makefile时会用到这个功能。

错误处理

make每执行一条命令就会返回一个状态码,值为零表明命令执行成功,若是某一行命令返回非零时,make就会中止执行。这种状况下能够用破折号前缀或者–keep going命令让make继续执行下去,可是并不建议这么作,除非能保证这一行命令的错误无伤大雅。

在以前长命令的例子中,分别使用&&和;做为链接符,当src存在时执行结果是没有区别的,但若是这个目录不存在呢,咱们将src改为srcs(不存在的目录)后看一下输出

#使用&&作链接符
cd srcs && \
    ls
/bin/sh: 1: cd: can't cd to srcs
make: *** [test] Error 2
#使用;做链接符
cd srcs; \
    ls
/bin/sh: 1: cd: can't cd to srcs
include  Makefile  src

能够看到,若是&&做链接符,其中某一条命令的错误会致使整个make的中止,而使用分号做链接符则不会由于中间某条命令的错误而使make中止。这里建议使用&&做链接符,在执行每一条命令以前都保证前一条命令执行成功。

另外一种避免错误的方式就是使用程序内置的错误处理机制,好比使用rm -f来替代rm,这样在删除不存在的文件时就不会报错,与此类似的还有mkdir -p等。

相关文章
相关标签/搜索