GNU make(2)

参考:

GNU Make学习总结(二)

变量

变量由一个前导符号$加上字符或者是括号字符组成, 名称区分大小写.

命名: 习惯上用全部大写字符表示常量, 小写字符表示变量, 单词之间用下划线.

变量赋值

有四种方式:

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

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

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

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

CC += gcc 同其他语言, 将右值增加到左边的变量后面.

除了单个变量, Makefile中还可以用define来定义一个宏来保存一组操作, 用来重用. 例子:

define newdir
mkdir tmp
cd tmp
endef

拓展变量

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

拓展规则:

make会分为两个阶段来完成工作,第一阶段读进makefile以及include进来的makefile,其中定义的变量和规则都会被加进make的内部数据库,并建立依存图,第二阶段会根据依存图判断需要进行哪些更新操作。

下面是用来处理何时扩展变量的几条准则:

  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的,如果一个命令超过一行,需要进行处理,下面举一个简单的例子
```makefile
.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等。

GNU make(2)的更多相关文章

  1. 感悟 GNU C 以及将 Vim 打造成 C/C++ 的半自动化 IDE

    C 语言在 Linux 系统中的重要性自然是无与伦比.不可替代,所以我写 Linux 江湖系列不可能不提 C 语言.C 语言是我的启蒙语言,感谢 C 语言带领我进入了程序世界.虽然现在不靠它吃饭,但是 ...

  2. 使用 GCC 和 GNU Binutils 编写能在 x86 实模式运行的 16 位代码

    不可否认,这次的标题有点长.之所以把标题写得这么详细,主要是为了搜索引擎能够准确地把确实需要了解 GCC 生成 16 位实模式代码方法的朋友带到我的博客.先说一下背景,编写能在 x86 实模式下运行的 ...

  3. 在 Linux 中使用 Eclipse 和 Gnu Autotools 管理 C/C++ 项目

    在我该系列的之前的所有随笔中,都是采用 Linux 发行版自带的包管理工具(如 apt-get.yum 等)进行软件的安装和卸载,从来没有向大家展示使用源代码自行编译安装软件的方法.但是长期混迹于 U ...

  4. GNU Readline 库及编程简介

    用过 Bash 命令行的一定知道,Bash 有几个特性: TAB 键可以用来命令补全 ↑ 或 ↓ 键可以用来快速输入历史命令 还有一些交互式行编辑快捷键: C-A / C-E 将光标移到行首/行尾 C ...

  5. GNU Radio Radar Toolbox

    GNU Radio Radar Toolbox Install guide Change to any folder in your home directory and enter followin ...

  6. gnu coreutils-8.25 for win32 static - Beta

    gnu.win32-coreutils-8.25.7z 2.7 Mb bc-1.06.tar.gz coreutils-8.25.tar.xz diffutils-3.5.tar.xz gawk-4. ...

  7. window下搭建c开发环境(GNU环境的安装)

    一.在windows平台上安装GNU环境 windows操作系统不自带GNU环境,如果需要开发跨平台的C语言程序,那么需要给windows安装GNU环境 windows下的两款GNU环境:MinGW和 ...

  8. GNU make使用变量⑤变量的引用、定义等

    在 Makefile 中,变量是一个名字(像是 C 语言中的宏),代表一个文本字符串(变量的值).在 Makefile 的目标.依赖.命令中引用变量的地方,变量会被它的值所取代(与 C 语言中宏引用的 ...

  9. (转)完全用GNU/Linux工作 by 王珢

    完全用GNU/Linux工作 王珢      (看完这篇博文,非常喜欢王珢的这篇博客,也我坚定了学gnu/linux的决心,并努力去按照国外的计算机思维模式去学习编程提高自己.看完这篇文章令我热血沸腾 ...

  10. Gnu/Linux的学习探索

    1.Gnu/Linux是一个基于POSIX和UNIX的多用户多任务 支持多线程多CPU的类UNIX的操作系统. 继承了UNIX以网络为核心的设计思想 是性能稳定的多用户网络操作系统. 1991年10月 ...

随机推荐

  1. linux磁盘存储管理基本命令和工具

    1 磁盘在linux表示方法 (1) IDE硬盘:hd[a~z]x,主设备号+次设备号+磁盘分区编号/hd(0-n,y) (2)SCSI硬盘:sd[a~z]x/hd(0-n,y) 注:主设备号可以唯一 ...

  2. 3-6局部变量的存储方式 & 3-7字符型字面值

    基础数据类型变量的存储 重点介绍方法级的变量,局势局部变量 存储中怎么存储呢? int n=100; 在栈中开辟内存存储空间. n是内存空间的别名 3-7字符型字面值 单引号不能丢,必须是英文状态. ...

  3. Qt开篇

    使用Qt两年有余,遇到问题多是现查现用,由于之前供职于一家保密性较强的单位,遇到的很多问题没有被记录下来.从今天开始,我会记记录自己的笔记.

  4. 详细分享UICollectionView的自定义布局(瀑布流, 线性, 圆形...)

    前言: 本篇文章不是分享collectionView的详细使用教程, 而是属于比较'高级'的collectionView使用技巧, 阅读之前, 我想你已经很熟悉collectionView的基本使用, ...

  5. Qt生成CSV 文件

    1.CSV 文件 不支持 EXCEL中 的多个工作表的模式. 一个 CVS 文件只能转换成 EXCEL 一个工作表. 2.逗号分隔值(Comma-Separated Values,CSV,有时也称为字 ...

  6. Weekly Contest 78-------->811. Subdomain Visit Count (split string with space and hash map)

    A website domain like "discuss.leetcode.com" consists of various subdomains. At the top le ...

  7. HDU 3499【最短路】

    题意: 给你一幅图,然后起点终点,然后有一个条件是可以使某条边的花费减半,求最短路的最小花费. 思路: (来自大哥) 最短路的时候多一维,途中是否有花费减半的边: 然后转移,如果上一条有减半的,这一条 ...

  8. 删除node_modul模块

    npm安装rimraf ,npm版本号要是低于5.x.x 具体不记得了,不然就安装不了这个工具 npm install rimraf -g 然后: rimraf node_modules 在这里学到的 ...

  9. 记一次因证书问题导致请求失败问题SSLHandshakeException

    记一次因证书问题导致请求失败问题SSLHandshakeException 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/10989813.html 最近接一外 ...

  10. java中数据的存放位置

    引用自java编程思想四----2.2.1 程序运行时,我们最好对数据保存到什么地方做到心中有数.特别要注意的是内存的分配.有六个地方都可以保存数据:(1) 寄存器.这是最快的保存区域,因为它位于和其 ...